codex-agent-view 0.4.5 → 0.4.7
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.ko.md +9 -6
- package/README.md +9 -6
- package/package.json +1 -1
- package/public/app.js +79 -11
- package/public/index.html +2 -0
- package/public/styles.css +65 -0
- package/skills/codex-agent-view/SKILL.md +13 -0
- package/src/core/monitor-store.mjs +267 -18
- package/src/core/normalize-hook-payload.mjs +13 -0
package/README.ko.md
CHANGED
|
@@ -10,10 +10,10 @@ Codex Agent View는 Codex가 지금 어떤 작업을 수행하고 있고 어떤
|
|
|
10
10
|
|
|
11
11
|
### 빠른 시작: 설치 후에는 Codex 앱 안에서만 사용
|
|
12
12
|
|
|
13
|
-
이 README는 `codex-agent-view@0.4.
|
|
13
|
+
이 README는 `codex-agent-view@0.4.7` 사용법을 설명한다. **최초 설치만** 일반 터미널에서 아래 exact-version 명령으로 진행한다.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install --global codex-agent-view@0.4.
|
|
16
|
+
npm install --global codex-agent-view@0.4.7
|
|
17
17
|
codex-agent-view install
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -40,7 +40,7 @@ Live UI의 기본 언어는 영어이며 language selector에서 **English**, **
|
|
|
40
40
|
|
|
41
41
|
### 현재 상태
|
|
42
42
|
|
|
43
|
-
`0.4.
|
|
43
|
+
`0.4.7`은 lifecycle 정확성 patch를 완성한다. `SessionEnd` 최종 우선순위, 늦은 event 차단과 정직한 **종료 미확인** 표시에 더해, 정상 또는 늦게 도착한 `SubagentStop`과 `PostToolUse`가 recent activity의 대응 start 항목을 **중지됨** 또는 **완료**로 갱신한다. 따라서 종료된 agent나 tool의 이전 recent activity가 잘못 **실행 중**으로 남지 않는다. 아래의 기존 제품 구성은 그대로 유지한다.
|
|
44
44
|
|
|
45
45
|
- 공식 Codex 앱의 내장 thread tools를 우선 사용하는 app-native active-task snapshot skill
|
|
46
46
|
- `.codex-plugin/plugin.json`, local marketplace catalog, genuine Codex skill
|
|
@@ -88,12 +88,15 @@ Codex Agent View는 historical audit이나 session replay 제품이 아니라
|
|
|
88
88
|
|
|
89
89
|
- 앱 안의 현재 task snapshot은 공식 Codex 앱이 제공하는 내장 thread tools의 explicit status와 `subAgentActivity`를 우선 사용한다.
|
|
90
90
|
- Hook event는 local monitor의 세부 lifecycle 상태에 대한 source of truth다. Monitor state는 bounded memory에만 있고 재시작하면 새 관찰 window가 시작된다. 별도 private viewer credential은 task history가 아니라 인증 metadata다.
|
|
91
|
+
- `Stop`은 관찰된 root turn과 session/work-item 요약을 즉시 `completed`로 표시한다. 진행 중이던 child agent나 tool은 자체 stop/tool completion 신호를 관찰하지 못했으므로 해당 row에서 별도로 `completion_not_observed`로 표시한다. `SessionEnd`는 terminal priority를 가지며, 그 시점에도 열려 있는 child agent·tool·permission은 완료로 추정하지 않고 `interrupted`로 표시한다.
|
|
92
|
+
- 공식 `SessionEnd` 전달은 최대 30분 지연될 수 있다. 종료 hook을 관찰하지 못한 채 활동이 열린 상태로 남으면 새 event가 없는 5분 뒤 `completion_not_observed`(**종료 미확인**)로 바꾸며 `completed`로 추정하지 않는다. 지연되거나 누락된 terminal event 때문에 오래된 활동을 완료·성공으로 잘못 표시하지 않기 위한 경계다.
|
|
91
93
|
- Read-only viewer credential은 한 설치 수명 동안 monitor 재시작과 upgrade를 넘어 유지되며, runtime/control token은 별도 process-scoped credential이다.
|
|
92
94
|
- 설치·trust·앱 재시작 뒤 첫 trusted hook이 backend를 자동 준비한다. 이는 monitor process 준비이며 Codex 화면이나 tab을 몰래 생성하는 기능이 아니다.
|
|
93
95
|
- 외부 telemetry, 원격 server, account, 필수 SQLite/영구 event store가 없다.
|
|
94
96
|
- 전체 prompt 원문, transcript path, 전체 tool input/output, assistant message를 monitor 상태나 UI에 저장·표시하지 않는다. 위에서 설명한 bounded/redacted 한 줄 작업 요약만 process memory에 유지할 수 있다.
|
|
95
97
|
- 기본 monitor는 진행 중인 작업과 참여 에이전트를 먼저 정렬하고 사람이 읽을 수 있는 label/status를 우선하며, raw ID와 event name은 주 정보로 표시하지 않는다. Live card에는 session ID를 표시하지 않는다.
|
|
96
98
|
- task/subagent 중지·재시작, message 전송, permission 자동 승인·거절 기능이 없다.
|
|
99
|
+
- Sender는 기존 bounded retry와 fail-open 동작을 유지한다. Disk queue나 persistent replay가 없으므로 hook budget 안에 전달하지 못한 event를 나중에 재생하지 않는다.
|
|
97
100
|
- 별도로 실행한 App Server는 앱 내장 thread tools와 다른 process다. 공식 앱의 live source로 간주하거나 둘을 같은 API로 설명하지 않는다.
|
|
98
101
|
|
|
99
102
|
별도로 실행한 Codex `0.146` App Server의 `thread/list` fallback도 실제 확인했지만 현재 root/subagent가 모두 `notLoaded`로 나타나 공식 앱의 live running/completed 상태를 공유하지 않았다. Persisted parent ID, alias, depth 보강은 가능했지만 live 판별에는 채택하지 않았다. `0.3.0`의 primary snapshot은 이 별도 server가 아니라 현재 공식 앱이 직접 제공하는 내장 thread tools를 사용한다.
|
|
@@ -211,16 +214,16 @@ Plugin enable/trust와 앱 재시작 뒤 생성되거나 재개되는 task는 tr
|
|
|
211
214
|
|
|
212
215
|
### npm 설치 명령 참고
|
|
213
216
|
|
|
214
|
-
아래 명령은 `0.4.
|
|
217
|
+
아래 명령은 `0.4.7`을 exact version으로 설치한다.
|
|
215
218
|
|
|
216
219
|
```bash
|
|
217
|
-
npm install --global codex-agent-view@0.4.
|
|
220
|
+
npm install --global codex-agent-view@0.4.7
|
|
218
221
|
codex-agent-view install
|
|
219
222
|
```
|
|
220
223
|
|
|
221
224
|
이 두 명령 뒤에는 Codex 앱을 완전히 다시 열고 Plugins 화면에서 설치·활성화와 hook trust를 확인한 다음 새 task를 만든다. 첫 trusted hook이 backend 준비와 event 전달을 내부 처리하므로 사용자가 monitor CLI를 실행하지 않는다. Plugin 카드의 **지금 사용해보기**로 `@codex-agent-view`를 선택하고 앱에서 `$show-agents`를 명시 선택한다. Panel을 닫은 뒤에도 같은 방식으로 skill을 다시 선택한다.
|
|
222
225
|
|
|
223
|
-
`0.4.
|
|
226
|
+
`0.4.7` 설치 경로는 위의 global package 설치와 명시적인 `codex-agent-view install` command 조합이다. 이후 일반 사용은 Codex 앱 안에서 진행한다. 이전의 유효한 설치에서 upgrade하면 installation-owned read-only viewer credential을 유지하며, `0.4.3`에서 검증한 legacy `0.4.2` migration 동작도 그대로 보존한다. Token과 private URL은 출력하지 않는다.
|
|
224
227
|
|
|
225
228
|
Version별 npm, install, migration, CI, tag와 GitHub Release evidence는 [docs/distribution.md](docs/distribution.md)에 보존한다. 각 evidence는 실제 확인한 뒤에만 갱신한다.
|
|
226
229
|
|
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ Codex Agent View gives you a clear, read-only view of what Codex is working on a
|
|
|
8
8
|
|
|
9
9
|
## Quick start: install once, then stay inside the Codex app
|
|
10
10
|
|
|
11
|
-
This README documents `codex-agent-view@0.4.
|
|
11
|
+
This README documents `codex-agent-view@0.4.7`. Use the exact-version command below for the one-time terminal installation.
|
|
12
12
|
|
|
13
13
|
Universal Plugins Directory search installation is not available yet, so use a regular terminal for the **initial installation only**:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install --global codex-agent-view@0.4.
|
|
16
|
+
npm install --global codex-agent-view@0.4.7
|
|
17
17
|
codex-agent-view install
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -40,7 +40,7 @@ In short: install once in a terminal; perform snapshot queries, status checks, l
|
|
|
40
40
|
|
|
41
41
|
## Status
|
|
42
42
|
|
|
43
|
-
Version `0.4.
|
|
43
|
+
Version `0.4.7` completes the lifecycle-correctness patch. In addition to terminal `SessionEnd`, late-event protection, and honest **End not confirmed** state, normal or late `SubagentStop` and `PostToolUse` events now refine the matching earlier start entry in recent activity to **Stopped** or **Completed**. A finished agent or tool therefore no longer leaves its prior recent-activity row falsely **Running**. It retains the app-native snapshot skill, privacy-minimized hooks, bounded in-memory reducer, local authenticated live backend, and explicit install/remove plus maintainer-diagnostic CLI commands.
|
|
44
44
|
|
|
45
45
|
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.
|
|
46
46
|
|
|
@@ -78,6 +78,8 @@ Codex Agent View is a live companion, not a historical audit or session-replay p
|
|
|
78
78
|
|
|
79
79
|
- The app-native current-task snapshot prioritizes explicit status and `subAgentActivity` from the official Codex app's built-in thread tools.
|
|
80
80
|
- Hooks remain the source of truth for detailed lifecycle state in the trusted-hook auto-prepared local live backend. Its operational state exists only in bounded process memory; restart begins a new observation window. The separate private viewer credential is authentication metadata, not stored task history.
|
|
81
|
+
- `Stop` marks the observed root turn and the session/work-item summary `completed` immediately. If a child agent or tool was still active, its own row is separately marked `completion_not_observed` because no child stop/tool completion signal was observed. `SessionEnd` has terminal priority; any child agent, tool, or permission still open at that point is shown as `interrupted`, not silently completed.
|
|
82
|
+
- Official `SessionEnd` delivery may be delayed by up to 30 minutes. If no ending hook is observed while activity still appears open, five minutes without a new event changes it to `completion_not_observed` (**End not confirmed**), never inferred `completed`. This keeps a delayed or missing terminal event from turning stale activity into a false success.
|
|
81
83
|
- The viewer credential is read-only and remains stable across monitor restarts and package upgrades during one installation. The runtime/control token remains separate and process-scoped.
|
|
82
84
|
- After installation, hook trust, and an app restart, the first trusted hook automatically prepares the backend. This prepares a local process; it does not create app UI without a user action.
|
|
83
85
|
- There is no external telemetry, remote server, account, required SQLite/persistent event store, or remote control.
|
|
@@ -85,6 +87,7 @@ Codex Agent View is a live companion, not a historical audit or session-replay p
|
|
|
85
87
|
- The default monitor sorts active work and participating agents first, uses human-readable labels and statuses as the primary presentation, and keeps raw IDs and event names out of the primary reading path. Session IDs are not shown in the live cards.
|
|
86
88
|
- The product cannot stop or restart tasks/subagents, send messages, or approve/deny permissions.
|
|
87
89
|
- Missing, duplicated, or out-of-order events remain visible as empty, unknown, or degraded state instead of being guessed away.
|
|
90
|
+
- The sender keeps its bounded retry and fail-open behavior. It has no disk-backed queue or persistent replay; an event that cannot be delivered within the hook budget is not replayed later.
|
|
88
91
|
|
|
89
92
|
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. That separate process is not the same as the built-in thread tools exposed directly by the current official app; `0.3.0` uses the latter for its primary snapshot.
|
|
90
93
|
|
|
@@ -163,16 +166,16 @@ After plugin enablement/trust and an app restart, the first trusted hook interna
|
|
|
163
166
|
|
|
164
167
|
## Install from npm
|
|
165
168
|
|
|
166
|
-
The commands below install `0.4.
|
|
169
|
+
The commands below install `0.4.7` by exact version.
|
|
167
170
|
|
|
168
171
|
```bash
|
|
169
|
-
npm install --global codex-agent-view@0.4.
|
|
172
|
+
npm install --global codex-agent-view@0.4.7
|
|
170
173
|
codex-agent-view install
|
|
171
174
|
```
|
|
172
175
|
|
|
173
176
|
After these two commands, fully reopen the Codex app, verify installation, enablement, and hook trust, then create a new task. The first trusted hook prepares the backend and delivers its event internally, so users do not run monitor CLI commands. Use the plugin card's **Quick start** action to select `@codex-agent-view`, then explicitly select `$show-agents` in the app. Repeat that explicit skill selection after closing the panel.
|
|
174
177
|
|
|
175
|
-
The `0.4.
|
|
178
|
+
The `0.4.7` installation path is the global package install followed by the explicit `codex-agent-view install` command above. Routine use remains inside the Codex app afterward. When upgrading an older valid installation, `install` preserves the installation-owned read-only viewer credential; the legacy `0.4.2` migration behavior remains as documented for `0.4.3`. Neither token nor the private URL is printed.
|
|
176
179
|
|
|
177
180
|
Version-specific npm, install, migration, CI, tag, and GitHub Release evidence is preserved in [Distribution](docs/distribution.md). That evidence is updated only after each item is actually verified.
|
|
178
181
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -4,7 +4,15 @@ const SESSION_TOKEN_KEY = "codex-agent-view-access-token";
|
|
|
4
4
|
const EXCLUDED_SESSION_KEY = "codex-agent-view-excluded-session";
|
|
5
5
|
const LANGUAGE_KEY = "codex-agent-view-language";
|
|
6
6
|
const SUPPORTED_LANGUAGES = new Set(["en", "ko", "es"]);
|
|
7
|
-
const KNOWN_STATUSES = new Set([
|
|
7
|
+
const KNOWN_STATUSES = new Set([
|
|
8
|
+
"running",
|
|
9
|
+
"waiting",
|
|
10
|
+
"completed",
|
|
11
|
+
"completion_not_observed",
|
|
12
|
+
"stale",
|
|
13
|
+
"interrupted",
|
|
14
|
+
"unknown",
|
|
15
|
+
]);
|
|
8
16
|
const VIEWER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
9
17
|
const CANONICAL_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
10
18
|
|
|
@@ -47,7 +55,13 @@ const MESSAGES = Object.freeze({
|
|
|
47
55
|
statusRunning: "Running",
|
|
48
56
|
statusWaiting: "Waiting",
|
|
49
57
|
statusCompleted: "Completed",
|
|
58
|
+
statusCompletionNotObserved: "End not confirmed",
|
|
59
|
+
statusStale: "Stale · end not confirmed",
|
|
60
|
+
statusInterrupted: "Interrupted",
|
|
50
61
|
statusUnknown: "Unknown",
|
|
62
|
+
completionNotObservedExplanation: "No end signal was received for this item, so completion cannot be confirmed.",
|
|
63
|
+
staleExplanation: "This status is out of date and no end signal was received. Completion cannot be confirmed.",
|
|
64
|
+
interruptedExplanation: "This activity was still open when the work ended; its own completion signal was not observed.",
|
|
51
65
|
connectingCopy: "Connecting to the Codex app's local status.",
|
|
52
66
|
sessionListAria: "Codex work list",
|
|
53
67
|
privacyPrompt: "This view shows only a shortened request summary; it never displays the full request or tool inputs.",
|
|
@@ -180,7 +194,13 @@ const MESSAGES = Object.freeze({
|
|
|
180
194
|
statusRunning: "실행 중",
|
|
181
195
|
statusWaiting: "대기",
|
|
182
196
|
statusCompleted: "완료",
|
|
197
|
+
statusCompletionNotObserved: "종료 확인 안 됨",
|
|
198
|
+
statusStale: "오래된 상태 · 종료 확인 안 됨",
|
|
199
|
+
statusInterrupted: "중단됨",
|
|
183
200
|
statusUnknown: "알 수 없음",
|
|
201
|
+
completionNotObservedExplanation: "이 항목의 종료 신호를 받지 못해 완료 여부를 확정할 수 없습니다.",
|
|
202
|
+
staleExplanation: "상태 정보가 오래되었고 종료 신호를 받지 못했습니다. 완료 여부를 확정할 수 없습니다.",
|
|
203
|
+
interruptedExplanation: "전체 작업이 끝날 때 이 활동이 열린 상태였습니다. 이 활동 자체의 완료 신호는 확인되지 않았습니다.",
|
|
184
204
|
connectingCopy: "Codex 앱의 로컬 상태에 연결하고 있습니다.",
|
|
185
205
|
sessionListAria: "Codex 작업 목록",
|
|
186
206
|
privacyPrompt: "이 화면은 짧게 줄인 요청 요약만 표시하며, 전체 요청이나 도구 입력은 표시하지 않습니다.",
|
|
@@ -313,7 +333,13 @@ const MESSAGES = Object.freeze({
|
|
|
313
333
|
statusRunning: "En ejecución",
|
|
314
334
|
statusWaiting: "En espera",
|
|
315
335
|
statusCompleted: "Completado",
|
|
336
|
+
statusCompletionNotObserved: "Fin no confirmado",
|
|
337
|
+
statusStale: "Estado desactualizado · fin no confirmado",
|
|
338
|
+
statusInterrupted: "Interrumpido",
|
|
316
339
|
statusUnknown: "Desconocido",
|
|
340
|
+
completionNotObservedExplanation: "No se recibió una señal de fin para este elemento, por lo que no se puede confirmar que haya terminado.",
|
|
341
|
+
staleExplanation: "El estado está desactualizado y no se recibió una señal de fin. No se puede confirmar que haya terminado.",
|
|
342
|
+
interruptedExplanation: "Esta actividad seguía abierta cuando terminó el trabajo; no se observó su propia señal de finalización.",
|
|
317
343
|
connectingCopy: "Conectando al estado local de la aplicación Codex.",
|
|
318
344
|
sessionListAria: "Lista de trabajos de Codex",
|
|
319
345
|
privacyPrompt: "Esta vista solo muestra un resumen abreviado de la solicitud; nunca muestra la solicitud completa ni las entradas de herramientas.",
|
|
@@ -414,14 +440,26 @@ const STATUS_KEYS = Object.freeze({
|
|
|
414
440
|
running: "statusRunning",
|
|
415
441
|
waiting: "statusWaiting",
|
|
416
442
|
completed: "statusCompleted",
|
|
443
|
+
completion_not_observed: "statusCompletionNotObserved",
|
|
444
|
+
stale: "statusStale",
|
|
445
|
+
interrupted: "statusInterrupted",
|
|
417
446
|
unknown: "statusUnknown",
|
|
418
447
|
});
|
|
419
448
|
|
|
420
449
|
const STATUS_ORDER = Object.freeze({
|
|
421
450
|
running: 0,
|
|
422
451
|
waiting: 1,
|
|
423
|
-
|
|
424
|
-
|
|
452
|
+
completion_not_observed: 2,
|
|
453
|
+
stale: 2,
|
|
454
|
+
interrupted: 3,
|
|
455
|
+
unknown: 4,
|
|
456
|
+
completed: 5,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const STATUS_EXPLANATION_KEYS = Object.freeze({
|
|
460
|
+
completion_not_observed: "completionNotObservedExplanation",
|
|
461
|
+
stale: "staleExplanation",
|
|
462
|
+
interrupted: "interruptedExplanation",
|
|
425
463
|
});
|
|
426
464
|
|
|
427
465
|
const ACTIVITY_KEYS = Object.freeze({
|
|
@@ -693,6 +731,9 @@ function normalizeCoreStatus(value) {
|
|
|
693
731
|
) {
|
|
694
732
|
return "completed";
|
|
695
733
|
}
|
|
734
|
+
if (value === "completion_not_observed" || value === "stale" || value === "interrupted") {
|
|
735
|
+
return value;
|
|
736
|
+
}
|
|
696
737
|
return normalizeStatus(value);
|
|
697
738
|
}
|
|
698
739
|
|
|
@@ -783,13 +824,13 @@ function normalizeDiagnostic(value) {
|
|
|
783
824
|
}
|
|
784
825
|
|
|
785
826
|
function deriveSessionStatus(session, agents, recentActivities) {
|
|
786
|
-
if (session.permission?.status === "waiting_for_user") {
|
|
787
|
-
return "waiting";
|
|
788
|
-
}
|
|
789
827
|
const reportedStatus = normalizeCoreStatus(session.status);
|
|
790
|
-
if (reportedStatus
|
|
828
|
+
if (reportedStatus !== "unknown") {
|
|
791
829
|
return reportedStatus;
|
|
792
830
|
}
|
|
831
|
+
if (session.permission?.status === "waiting_for_user") {
|
|
832
|
+
return "waiting";
|
|
833
|
+
}
|
|
793
834
|
if (
|
|
794
835
|
agents.some((agent) => agent.status === "running") ||
|
|
795
836
|
recentActivities[0]?.status === "running"
|
|
@@ -924,12 +965,24 @@ function createStatusBadge(status) {
|
|
|
924
965
|
dot.setAttribute("aria-hidden", "true");
|
|
925
966
|
|
|
926
967
|
const label = document.createElement("span");
|
|
927
|
-
label.textContent = t(STATUS_KEYS[status]);
|
|
968
|
+
label.textContent = t(STATUS_KEYS[status] ?? "statusUnknown");
|
|
928
969
|
|
|
929
970
|
badge.append(dot, label);
|
|
930
971
|
return badge;
|
|
931
972
|
}
|
|
932
973
|
|
|
974
|
+
function createStatusExplanation(status) {
|
|
975
|
+
const messageKey = STATUS_EXPLANATION_KEYS[status];
|
|
976
|
+
if (!messageKey) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const explanation = document.createElement("p");
|
|
981
|
+
explanation.className = "status-explanation";
|
|
982
|
+
explanation.textContent = t(messageKey);
|
|
983
|
+
return explanation;
|
|
984
|
+
}
|
|
985
|
+
|
|
933
986
|
function createTime(timestampMs, prefix) {
|
|
934
987
|
const wrapper = document.createElement("span");
|
|
935
988
|
wrapper.className = "time-label";
|
|
@@ -1006,10 +1059,16 @@ function createAgentItem(agent) {
|
|
|
1006
1059
|
document.createTextNode(` · ${formatDuration(agent.startedAtMs, agent.stoppedAtMs)}`),
|
|
1007
1060
|
);
|
|
1008
1061
|
|
|
1062
|
+
const statusExplanation = createStatusExplanation(agent.status);
|
|
1063
|
+
|
|
1009
1064
|
const technicalRows = [[t("agentId"), agent.agentId]];
|
|
1010
1065
|
technicalRows.push([t("rawProfile"), agent.agentType]);
|
|
1011
1066
|
|
|
1012
|
-
item.append(heading, metadata
|
|
1067
|
+
item.append(heading, metadata);
|
|
1068
|
+
if (statusExplanation) {
|
|
1069
|
+
item.append(statusExplanation);
|
|
1070
|
+
}
|
|
1071
|
+
item.append(createTechnicalInfo(technicalRows));
|
|
1013
1072
|
return item;
|
|
1014
1073
|
}
|
|
1015
1074
|
|
|
@@ -1081,6 +1140,10 @@ function createSessionCard(session) {
|
|
|
1081
1140
|
createStatusBadge(session.status),
|
|
1082
1141
|
createTime(session.lastActivityAtMs, t("recentActivity")),
|
|
1083
1142
|
);
|
|
1143
|
+
const statusExplanation = createStatusExplanation(session.status);
|
|
1144
|
+
if (statusExplanation) {
|
|
1145
|
+
sessionState.append(statusExplanation);
|
|
1146
|
+
}
|
|
1084
1147
|
|
|
1085
1148
|
cardHeader.append(identity, sessionState);
|
|
1086
1149
|
|
|
@@ -1156,10 +1219,15 @@ function sessionMatchesQuery(session, query) {
|
|
|
1156
1219
|
}
|
|
1157
1220
|
|
|
1158
1221
|
function sessionMatchesStatus(session, status) {
|
|
1222
|
+
const matchesStatus = (candidate) => (
|
|
1223
|
+
candidate === status ||
|
|
1224
|
+
(status === "completion_not_observed" && candidate === "stale")
|
|
1225
|
+
);
|
|
1159
1226
|
return (
|
|
1160
1227
|
status === "all" ||
|
|
1161
|
-
session.status
|
|
1162
|
-
session.agents.some((agent) => agent.status
|
|
1228
|
+
matchesStatus(session.status) ||
|
|
1229
|
+
session.agents.some((agent) => matchesStatus(agent.status)) ||
|
|
1230
|
+
session.recentActivities.some((activity) => matchesStatus(activity.status))
|
|
1163
1231
|
);
|
|
1164
1232
|
}
|
|
1165
1233
|
|
package/public/index.html
CHANGED
|
@@ -129,6 +129,8 @@
|
|
|
129
129
|
<option value="running" data-i18n="statusRunning">Running</option>
|
|
130
130
|
<option value="waiting" data-i18n="statusWaiting">Waiting</option>
|
|
131
131
|
<option value="completed" data-i18n="statusCompleted">Completed</option>
|
|
132
|
+
<option value="completion_not_observed" data-i18n="statusCompletionNotObserved">End not confirmed</option>
|
|
133
|
+
<option value="interrupted" data-i18n="statusInterrupted">Interrupted</option>
|
|
132
134
|
<option value="unknown" data-i18n="statusUnknown">Unknown</option>
|
|
133
135
|
</select>
|
|
134
136
|
</div>
|
package/public/styles.css
CHANGED
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
--waiting-soft: #fff0ce;
|
|
26
26
|
--completed: #45605f;
|
|
27
27
|
--completed-soft: #e5eceb;
|
|
28
|
+
--unconfirmed: #8a4f18;
|
|
29
|
+
--unconfirmed-soft: #f9e8d6;
|
|
30
|
+
--interrupted: #765226;
|
|
31
|
+
--interrupted-soft: #f2e8d9;
|
|
28
32
|
--unknown: #6b6678;
|
|
29
33
|
--unknown-soft: #ece9f2;
|
|
30
34
|
--danger: #a13838;
|
|
@@ -597,6 +601,16 @@ h1 {
|
|
|
597
601
|
background: linear-gradient(110deg, var(--running-soft), var(--surface-raised) 72%);
|
|
598
602
|
}
|
|
599
603
|
|
|
604
|
+
.session-card[data-status="completion_not_observed"],
|
|
605
|
+
.session-card[data-status="stale"] {
|
|
606
|
+
border-color: color-mix(in srgb, var(--unconfirmed) 52%, var(--border-default));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.session-card[data-status="completion_not_observed"] .session-header,
|
|
610
|
+
.session-card[data-status="stale"] .session-header {
|
|
611
|
+
background: linear-gradient(110deg, var(--unconfirmed-soft), var(--surface-raised) 72%);
|
|
612
|
+
}
|
|
613
|
+
|
|
600
614
|
.session-header {
|
|
601
615
|
display: flex;
|
|
602
616
|
align-items: center;
|
|
@@ -666,6 +680,17 @@ h1 {
|
|
|
666
680
|
background: var(--completed-soft);
|
|
667
681
|
}
|
|
668
682
|
|
|
683
|
+
.status-badge[data-status="completion_not_observed"],
|
|
684
|
+
.status-badge[data-status="stale"] {
|
|
685
|
+
color: var(--unconfirmed);
|
|
686
|
+
background: var(--unconfirmed-soft);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.status-badge[data-status="interrupted"] {
|
|
690
|
+
color: var(--interrupted);
|
|
691
|
+
background: var(--interrupted-soft);
|
|
692
|
+
}
|
|
693
|
+
|
|
669
694
|
.status-badge[data-status="running"] .status-dot,
|
|
670
695
|
.activity-marker[data-status="running"] {
|
|
671
696
|
background: var(--running);
|
|
@@ -681,6 +706,31 @@ h1 {
|
|
|
681
706
|
background: var(--completed);
|
|
682
707
|
}
|
|
683
708
|
|
|
709
|
+
.status-badge[data-status="completion_not_observed"] .status-dot,
|
|
710
|
+
.status-badge[data-status="stale"] .status-dot,
|
|
711
|
+
.activity-marker[data-status="completion_not_observed"],
|
|
712
|
+
.activity-marker[data-status="stale"] {
|
|
713
|
+
background: var(--unconfirmed);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.status-badge[data-status="interrupted"] .status-dot,
|
|
717
|
+
.activity-marker[data-status="interrupted"] {
|
|
718
|
+
background: var(--interrupted);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.status-explanation {
|
|
722
|
+
max-width: 34rem;
|
|
723
|
+
margin: 0;
|
|
724
|
+
color: var(--text-secondary);
|
|
725
|
+
font-size: var(--text-xs);
|
|
726
|
+
line-height: 1.4;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.session-state .status-explanation {
|
|
730
|
+
max-width: 24rem;
|
|
731
|
+
text-align: right;
|
|
732
|
+
}
|
|
733
|
+
|
|
684
734
|
.time-label,
|
|
685
735
|
.agent-metadata,
|
|
686
736
|
.activity-metadata {
|
|
@@ -726,6 +776,13 @@ h1 {
|
|
|
726
776
|
border-color: color-mix(in srgb, var(--running) 42%, var(--border-default));
|
|
727
777
|
}
|
|
728
778
|
|
|
779
|
+
.agent-item[data-status="completion_not_observed"],
|
|
780
|
+
.agent-item[data-status="stale"],
|
|
781
|
+
.agent-item[data-status="interrupted"] {
|
|
782
|
+
background: color-mix(in srgb, var(--unconfirmed-soft) 58%, var(--surface-raised));
|
|
783
|
+
border-color: color-mix(in srgb, var(--unconfirmed) 38%, var(--border-default));
|
|
784
|
+
}
|
|
785
|
+
|
|
729
786
|
.agent-heading {
|
|
730
787
|
display: flex;
|
|
731
788
|
align-items: center;
|
|
@@ -933,6 +990,10 @@ footer p {
|
|
|
933
990
|
align-items: flex-start;
|
|
934
991
|
}
|
|
935
992
|
|
|
993
|
+
.session-state .status-explanation {
|
|
994
|
+
text-align: left;
|
|
995
|
+
}
|
|
996
|
+
|
|
936
997
|
.session-header {
|
|
937
998
|
gap: var(--space-3);
|
|
938
999
|
padding: var(--space-4);
|
|
@@ -980,6 +1041,10 @@ footer p {
|
|
|
980
1041
|
--waiting-soft: #493819;
|
|
981
1042
|
--completed: #a9bfbb;
|
|
982
1043
|
--completed-soft: #34433f;
|
|
1044
|
+
--unconfirmed: #f2b879;
|
|
1045
|
+
--unconfirmed-soft: #493421;
|
|
1046
|
+
--interrupted: #dcc09a;
|
|
1047
|
+
--interrupted-soft: #423729;
|
|
983
1048
|
--unknown: #bbb3ca;
|
|
984
1049
|
--unknown-soft: #3c3746;
|
|
985
1050
|
--danger: #f19a9a;
|
|
@@ -118,6 +118,19 @@ means that monitor process observed no hook events; it does not prove that the
|
|
|
118
118
|
Codex app has no tasks. Restarting the in-memory monitor begins a new bounded
|
|
119
119
|
observation window.
|
|
120
120
|
|
|
121
|
+
When a CLI or live-monitor snapshot returns lifecycle statuses, preserve their
|
|
122
|
+
meaning exactly:
|
|
123
|
+
|
|
124
|
+
- A session/work-item `completed` status is grounded in an observed `Stop` or
|
|
125
|
+
terminal `SessionEnd`; report it as observed completion, not inferred
|
|
126
|
+
success.
|
|
127
|
+
- `completion_not_observed` means active state had no new event for the default
|
|
128
|
+
five-minute window and no ending hook was observed. Render it as **End not
|
|
129
|
+
confirmed**. Never reinterpret it as either `running` or `completed`.
|
|
130
|
+
- `interrupted` means the parent/session became terminal while a child agent or
|
|
131
|
+
tool had no own stop/completion signal. Never rewrite it as `running` or
|
|
132
|
+
`completed`, and do not invent a success or failure result.
|
|
133
|
+
|
|
121
134
|
After explicit installation, hook review/trust, and a Codex app restart, the
|
|
122
135
|
first trusted hook normally prepares the local backend internally and retries
|
|
123
136
|
delivery of that same privacy-minimized event. The user never registers a task
|
|
@@ -5,6 +5,7 @@ const DEFAULT_LIMITS = Object.freeze({
|
|
|
5
5
|
maxAgentsPerSession: 100,
|
|
6
6
|
maxDiagnostics: 100,
|
|
7
7
|
maxSessions: 50,
|
|
8
|
+
staleAfterMs: 5 * 60 * 1000,
|
|
8
9
|
});
|
|
9
10
|
|
|
10
11
|
function positiveInteger(value, name) {
|
|
@@ -74,7 +75,30 @@ function applyWorkspaceLabel(session, event) {
|
|
|
74
75
|
session.workspace_label_observed_at_ms = event.received_at_ms;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function
|
|
78
|
+
function hasActiveState(session) {
|
|
79
|
+
return (
|
|
80
|
+
session.permission.status === "waiting_for_user" ||
|
|
81
|
+
session.root_turn.status === "running" ||
|
|
82
|
+
[...session.agents.values()].some(({ status }) => status === "running") ||
|
|
83
|
+
[...session.tools.values()].some(({ status }) => status === "running")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isStaleActiveSession(session, nowMs, staleAfterMs) {
|
|
88
|
+
return (
|
|
89
|
+
!session.lifecycle.end_observed &&
|
|
90
|
+
hasActiveState(session) &&
|
|
91
|
+
nowMs - session.last_seen_at_ms >= staleAfterMs
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function deriveSessionStatus(session, nowMs, staleAfterMs) {
|
|
96
|
+
if (session.lifecycle.end_observed) {
|
|
97
|
+
return "completed";
|
|
98
|
+
}
|
|
99
|
+
if (isStaleActiveSession(session, nowMs, staleAfterMs)) {
|
|
100
|
+
return "completion_not_observed";
|
|
101
|
+
}
|
|
78
102
|
if (session.permission.status === "waiting_for_user") {
|
|
79
103
|
return "waiting_for_user";
|
|
80
104
|
}
|
|
@@ -85,12 +109,92 @@ function deriveSessionStatus(session) {
|
|
|
85
109
|
) {
|
|
86
110
|
return "running";
|
|
87
111
|
}
|
|
88
|
-
if (session.
|
|
112
|
+
if (session.root_turn.status === "completed") {
|
|
89
113
|
return "completed";
|
|
90
114
|
}
|
|
91
115
|
return "observed";
|
|
92
116
|
}
|
|
93
117
|
|
|
118
|
+
function settleRunningState(session, status, options = {}) {
|
|
119
|
+
const turnId = options.turnId;
|
|
120
|
+
const settledAgentIds = new Set();
|
|
121
|
+
const settledToolUseIds = new Set();
|
|
122
|
+
for (const agent of session.agents.values()) {
|
|
123
|
+
if (
|
|
124
|
+
agent.status === "running" &&
|
|
125
|
+
(turnId === undefined || agent.turn_id === turnId)
|
|
126
|
+
) {
|
|
127
|
+
agent.status = status;
|
|
128
|
+
settledAgentIds.add(agent.agent_id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const tool of session.tools.values()) {
|
|
132
|
+
if (
|
|
133
|
+
tool.status === "running" &&
|
|
134
|
+
(turnId === undefined || tool.turn_id === turnId)
|
|
135
|
+
) {
|
|
136
|
+
tool.status = status;
|
|
137
|
+
settledToolUseIds.add(tool.tool_use_id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const activity of session.recent_activities) {
|
|
141
|
+
if (
|
|
142
|
+
activity.status === "running" &&
|
|
143
|
+
((activity.type === "subagent_started" &&
|
|
144
|
+
settledAgentIds.has(activity.agent_id)) ||
|
|
145
|
+
(activity.type === "tool_started" &&
|
|
146
|
+
settledToolUseIds.has(activity.tool_use_id)))
|
|
147
|
+
) {
|
|
148
|
+
activity.status = status;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function clearPermission(session, activityStatus) {
|
|
154
|
+
const permission = session.permission;
|
|
155
|
+
if (permission.status !== "waiting_for_user") {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const activity of session.recent_activities) {
|
|
159
|
+
if (
|
|
160
|
+
activity.type === "permission_requested" &&
|
|
161
|
+
activity.status === "waiting_for_user" &&
|
|
162
|
+
activity.turn_id === permission.turn_id &&
|
|
163
|
+
activity.tool_name === permission.tool_name
|
|
164
|
+
) {
|
|
165
|
+
activity.status = activityStatus;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
session.permission = { status: "idle" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function settleRootTurnActivity(session, status) {
|
|
173
|
+
for (const activity of session.recent_activities) {
|
|
174
|
+
if (
|
|
175
|
+
activity.type === "turn_started" &&
|
|
176
|
+
activity.status === "running" &&
|
|
177
|
+
activity.turn_id === session.root_turn.turn_id
|
|
178
|
+
) {
|
|
179
|
+
activity.status = status;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function resetTransientState(session) {
|
|
186
|
+
session.agents.clear();
|
|
187
|
+
session.tools.clear();
|
|
188
|
+
session.root_turn = {
|
|
189
|
+
status: "idle",
|
|
190
|
+
turn_id: null,
|
|
191
|
+
started_at_ms: null,
|
|
192
|
+
stopped_at_ms: null,
|
|
193
|
+
has_out_of_order_events: false,
|
|
194
|
+
};
|
|
195
|
+
session.permission = { status: "idle" };
|
|
196
|
+
}
|
|
197
|
+
|
|
94
198
|
function touchMapEntry(map, key, value) {
|
|
95
199
|
map.delete(key);
|
|
96
200
|
map.set(key, value);
|
|
@@ -109,7 +213,14 @@ function addActivity(session, event, status, limit) {
|
|
|
109
213
|
received_at_ms: event.received_at_ms,
|
|
110
214
|
};
|
|
111
215
|
|
|
112
|
-
for (const field of [
|
|
216
|
+
for (const field of [
|
|
217
|
+
"turn_id",
|
|
218
|
+
"agent_id",
|
|
219
|
+
"agent_type",
|
|
220
|
+
"tool_name",
|
|
221
|
+
"tool_use_id",
|
|
222
|
+
"session_start_source",
|
|
223
|
+
]) {
|
|
113
224
|
if (field in event) {
|
|
114
225
|
activity[field] = event[field];
|
|
115
226
|
}
|
|
@@ -121,15 +232,60 @@ function addActivity(session, event, status, limit) {
|
|
|
121
232
|
}
|
|
122
233
|
}
|
|
123
234
|
|
|
235
|
+
function refineUnresolvedStartActivity(
|
|
236
|
+
session,
|
|
237
|
+
{ type, idField, id, status },
|
|
238
|
+
) {
|
|
239
|
+
const unresolvedStatuses = new Set([
|
|
240
|
+
"running",
|
|
241
|
+
"completion_not_observed",
|
|
242
|
+
"interrupted",
|
|
243
|
+
]);
|
|
244
|
+
for (const activity of session.recent_activities) {
|
|
245
|
+
if (
|
|
246
|
+
activity.type === type &&
|
|
247
|
+
activity[idField] === id &&
|
|
248
|
+
unresolvedStatuses.has(activity.status)
|
|
249
|
+
) {
|
|
250
|
+
activity.status = status;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
124
256
|
function applySessionEvent(session, event, limits) {
|
|
125
257
|
const lifecycle = session.lifecycle;
|
|
126
258
|
if (event.type === "session_started") {
|
|
127
259
|
const resumedAfterEnd = lifecycle.end_observed;
|
|
260
|
+
const startsNewEpoch =
|
|
261
|
+
resumedAfterEnd ||
|
|
262
|
+
event.session_start_source === "resume" ||
|
|
263
|
+
event.session_start_source === "clear";
|
|
264
|
+
if (event.session_start_source === "compact") {
|
|
265
|
+
if (resumedAfterEnd) {
|
|
266
|
+
return "stale";
|
|
267
|
+
}
|
|
268
|
+
lifecycle.start_observed = true;
|
|
269
|
+
lifecycle.started_at_ms ??= event.received_at_ms;
|
|
270
|
+
addActivity(session, event, "observed", limits.maxActivitiesPerSession);
|
|
271
|
+
return "applied";
|
|
272
|
+
}
|
|
273
|
+
if (lifecycle.start_observed && !startsNewEpoch) {
|
|
274
|
+
return "duplicate";
|
|
275
|
+
}
|
|
276
|
+
if (startsNewEpoch) {
|
|
277
|
+
if (!resumedAfterEnd) {
|
|
278
|
+
settleRootTurnActivity(session, "completion_not_observed");
|
|
279
|
+
clearPermission(session, "completion_not_observed");
|
|
280
|
+
settleRunningState(session, "completion_not_observed");
|
|
281
|
+
}
|
|
282
|
+
resetTransientState(session);
|
|
283
|
+
}
|
|
128
284
|
lifecycle.start_observed = true;
|
|
129
|
-
lifecycle.started_at_ms
|
|
285
|
+
lifecycle.started_at_ms = event.received_at_ms;
|
|
130
286
|
lifecycle.end_observed = false;
|
|
131
287
|
lifecycle.ended_at_ms = null;
|
|
132
|
-
lifecycle.has_out_of_order_events
|
|
288
|
+
lifecycle.has_out_of_order_events = false;
|
|
133
289
|
addActivity(session, event, "observed", limits.maxActivitiesPerSession);
|
|
134
290
|
return "applied";
|
|
135
291
|
}
|
|
@@ -140,9 +296,11 @@ function applySessionEvent(session, event, limits) {
|
|
|
140
296
|
lifecycle.end_observed = true;
|
|
141
297
|
lifecycle.ended_at_ms = event.received_at_ms;
|
|
142
298
|
lifecycle.has_out_of_order_events = !lifecycle.start_observed;
|
|
299
|
+
settleRootTurnActivity(session, "completed");
|
|
143
300
|
session.root_turn.status = "completed";
|
|
144
301
|
session.root_turn.stopped_at_ms ??= event.received_at_ms;
|
|
145
|
-
session
|
|
302
|
+
clearPermission(session, "interrupted");
|
|
303
|
+
settleRunningState(session, "interrupted");
|
|
146
304
|
addActivity(session, event, "completed", limits.maxActivitiesPerSession);
|
|
147
305
|
return "applied";
|
|
148
306
|
}
|
|
@@ -150,9 +308,10 @@ function applySessionEvent(session, event, limits) {
|
|
|
150
308
|
function applyTurnEvent(session, event, limits) {
|
|
151
309
|
const turn = session.root_turn;
|
|
152
310
|
if (event.type === "turn_started") {
|
|
153
|
-
if (turn.turn_id === event.turn_id
|
|
154
|
-
return "duplicate";
|
|
311
|
+
if (turn.turn_id === event.turn_id) {
|
|
312
|
+
return turn.status === "running" ? "duplicate" : "stale";
|
|
155
313
|
}
|
|
314
|
+
settleRunningState(session, "completion_not_observed");
|
|
156
315
|
session.root_turn = {
|
|
157
316
|
status: "running",
|
|
158
317
|
turn_id: event.turn_id,
|
|
@@ -160,7 +319,12 @@ function applyTurnEvent(session, event, limits) {
|
|
|
160
319
|
stopped_at_ms: null,
|
|
161
320
|
has_out_of_order_events: false,
|
|
162
321
|
};
|
|
163
|
-
|
|
322
|
+
if (
|
|
323
|
+
session.permission.status !== "waiting_for_user" ||
|
|
324
|
+
session.permission.turn_id !== event.turn_id
|
|
325
|
+
) {
|
|
326
|
+
clearPermission(session, "completion_not_observed");
|
|
327
|
+
}
|
|
164
328
|
addActivity(session, event, "running", limits.maxActivitiesPerSession);
|
|
165
329
|
return "applied";
|
|
166
330
|
}
|
|
@@ -169,6 +333,9 @@ function applyTurnEvent(session, event, limits) {
|
|
|
169
333
|
return "duplicate";
|
|
170
334
|
}
|
|
171
335
|
const startObserved = turn.turn_id === event.turn_id && turn.started_at_ms !== null;
|
|
336
|
+
if (startObserved) {
|
|
337
|
+
settleRootTurnActivity(session, "completed");
|
|
338
|
+
}
|
|
172
339
|
session.root_turn = {
|
|
173
340
|
status: "completed",
|
|
174
341
|
turn_id: event.turn_id,
|
|
@@ -176,7 +343,15 @@ function applyTurnEvent(session, event, limits) {
|
|
|
176
343
|
stopped_at_ms: event.received_at_ms,
|
|
177
344
|
has_out_of_order_events: !startObserved,
|
|
178
345
|
};
|
|
179
|
-
|
|
346
|
+
if (
|
|
347
|
+
session.permission.status === "waiting_for_user" &&
|
|
348
|
+
session.permission.turn_id === event.turn_id
|
|
349
|
+
) {
|
|
350
|
+
clearPermission(session, "completion_not_observed");
|
|
351
|
+
}
|
|
352
|
+
settleRunningState(session, "completion_not_observed", {
|
|
353
|
+
turnId: event.turn_id,
|
|
354
|
+
});
|
|
180
355
|
addActivity(
|
|
181
356
|
session,
|
|
182
357
|
event,
|
|
@@ -192,6 +367,7 @@ function applySubagentEvent(session, event, limits) {
|
|
|
192
367
|
agent = {
|
|
193
368
|
agent_id: event.agent_id,
|
|
194
369
|
agent_type: event.agent_type,
|
|
370
|
+
turn_id: event.turn_id,
|
|
195
371
|
status: "unknown",
|
|
196
372
|
started_at_ms: null,
|
|
197
373
|
stopped_at_ms: null,
|
|
@@ -222,9 +398,18 @@ function applySubagentEvent(session, event, limits) {
|
|
|
222
398
|
agent.stopped_at_ms = event.received_at_ms;
|
|
223
399
|
agent.status = agent.start_observed ? "stopped" : "stopped_without_start";
|
|
224
400
|
agent.has_out_of_order_events = !agent.start_observed;
|
|
401
|
+
if (agent.start_observed) {
|
|
402
|
+
refineUnresolvedStartActivity(session, {
|
|
403
|
+
type: "subagent_started",
|
|
404
|
+
idField: "agent_id",
|
|
405
|
+
id: event.agent_id,
|
|
406
|
+
status: "stopped",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
225
409
|
}
|
|
226
410
|
|
|
227
411
|
agent.agent_type = event.agent_type;
|
|
412
|
+
agent.turn_id = event.turn_id;
|
|
228
413
|
agent.last_seen_at_ms = Math.max(agent.last_seen_at_ms, event.received_at_ms);
|
|
229
414
|
touchMapEntry(session.agents, event.agent_id, agent);
|
|
230
415
|
trimMap(session.agents, limits.maxAgentsPerSession);
|
|
@@ -274,6 +459,14 @@ function applyToolEvent(session, event, limits) {
|
|
|
274
459
|
tool.status = tool.start_observed ? "completed" : "completed_without_start";
|
|
275
460
|
tool.has_out_of_order_events = !tool.start_observed;
|
|
276
461
|
activityStatus = tool.status;
|
|
462
|
+
if (tool.start_observed) {
|
|
463
|
+
refineUnresolvedStartActivity(session, {
|
|
464
|
+
type: "tool_started",
|
|
465
|
+
idField: "tool_use_id",
|
|
466
|
+
id: event.tool_use_id,
|
|
467
|
+
status: "completed",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
277
470
|
|
|
278
471
|
const permission = session.permission;
|
|
279
472
|
if (
|
|
@@ -282,7 +475,7 @@ function applyToolEvent(session, event, limits) {
|
|
|
282
475
|
permission.turn_id === event.turn_id &&
|
|
283
476
|
event.received_at_ms >= permission.requested_at_ms
|
|
284
477
|
) {
|
|
285
|
-
session
|
|
478
|
+
clearPermission(session, "completed");
|
|
286
479
|
}
|
|
287
480
|
}
|
|
288
481
|
|
|
@@ -328,9 +521,29 @@ function applyPermissionEvent(session, event, limits) {
|
|
|
328
521
|
}
|
|
329
522
|
|
|
330
523
|
function applyEvent(session, event, limits) {
|
|
331
|
-
if (event.type === "session_started"
|
|
524
|
+
if (event.type === "session_started") {
|
|
525
|
+
return applySessionEvent(session, event, limits);
|
|
526
|
+
}
|
|
527
|
+
if (
|
|
528
|
+
session.lifecycle.end_observed &&
|
|
529
|
+
event.type !== "tool_completed" &&
|
|
530
|
+
event.type !== "subagent_stopped"
|
|
531
|
+
) {
|
|
532
|
+
return event.type === "session_ended" ? "duplicate" : "stale";
|
|
533
|
+
}
|
|
534
|
+
if (event.type === "session_ended") {
|
|
332
535
|
return applySessionEvent(session, event, limits);
|
|
333
536
|
}
|
|
537
|
+
if (
|
|
538
|
+
session.root_turn.status === "completed" &&
|
|
539
|
+
session.root_turn.turn_id === event.turn_id &&
|
|
540
|
+
(event.type === "turn_started" ||
|
|
541
|
+
event.type === "permission_requested" ||
|
|
542
|
+
event.type === "tool_started" ||
|
|
543
|
+
event.type === "subagent_started")
|
|
544
|
+
) {
|
|
545
|
+
return "stale";
|
|
546
|
+
}
|
|
334
547
|
if (event.type === "turn_started" || event.type === "turn_stopped") {
|
|
335
548
|
return applyTurnEvent(session, event, limits);
|
|
336
549
|
}
|
|
@@ -343,22 +556,51 @@ function applyEvent(session, event, limits) {
|
|
|
343
556
|
return applyPermissionEvent(session, event, limits);
|
|
344
557
|
}
|
|
345
558
|
|
|
346
|
-
function snapshotSession(session) {
|
|
559
|
+
function snapshotSession(session, nowMs, staleAfterMs) {
|
|
560
|
+
const staleActive = isStaleActiveSession(session, nowMs, staleAfterMs);
|
|
347
561
|
return {
|
|
348
562
|
session_id: session.session_id,
|
|
349
563
|
workspace_label: session.workspace_label,
|
|
350
564
|
task_summary: session.task_summary,
|
|
351
|
-
status: deriveSessionStatus(session),
|
|
565
|
+
status: deriveSessionStatus(session, nowMs, staleAfterMs),
|
|
352
566
|
first_seen_at_ms: session.first_seen_at_ms,
|
|
353
567
|
last_seen_at_ms: session.last_seen_at_ms,
|
|
354
568
|
agents: [...session.agents.values()]
|
|
355
|
-
.map(({ start_observed, stop_observed, ...agent }) => ({
|
|
569
|
+
.map(({ start_observed, stop_observed, ...agent }) => ({
|
|
570
|
+
...agent,
|
|
571
|
+
...(staleActive && agent.status === "running"
|
|
572
|
+
? { status: "completion_not_observed" }
|
|
573
|
+
: {}),
|
|
574
|
+
}))
|
|
356
575
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
357
|
-
|
|
576
|
+
tools: [...session.tools.values()]
|
|
577
|
+
.map(({ start_observed, completion_observed, ...tool }) => ({
|
|
578
|
+
...tool,
|
|
579
|
+
...(staleActive && tool.status === "running"
|
|
580
|
+
? { status: "completion_not_observed" }
|
|
581
|
+
: {}),
|
|
582
|
+
}))
|
|
583
|
+
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
584
|
+
root_turn: {
|
|
585
|
+
...session.root_turn,
|
|
586
|
+
...(staleActive && session.root_turn.status === "running"
|
|
587
|
+
? { status: "completion_not_observed" }
|
|
588
|
+
: {}),
|
|
589
|
+
},
|
|
358
590
|
recent_activities: session.recent_activities.map((activity) => ({
|
|
359
591
|
...activity,
|
|
592
|
+
...(staleActive &&
|
|
593
|
+
(activity.status === "running" ||
|
|
594
|
+
activity.status === "waiting_for_user")
|
|
595
|
+
? { status: "completion_not_observed" }
|
|
596
|
+
: {}),
|
|
360
597
|
})),
|
|
361
|
-
permission: {
|
|
598
|
+
permission: {
|
|
599
|
+
...session.permission,
|
|
600
|
+
...(staleActive && session.permission.status === "waiting_for_user"
|
|
601
|
+
? { status: "completion_not_observed" }
|
|
602
|
+
: {}),
|
|
603
|
+
},
|
|
362
604
|
};
|
|
363
605
|
}
|
|
364
606
|
|
|
@@ -381,6 +623,10 @@ export function createMonitorStore(options = {}) {
|
|
|
381
623
|
options.maxSessions ?? DEFAULT_LIMITS.maxSessions,
|
|
382
624
|
"maxSessions",
|
|
383
625
|
),
|
|
626
|
+
staleAfterMs: positiveInteger(
|
|
627
|
+
options.staleAfterMs ?? DEFAULT_LIMITS.staleAfterMs,
|
|
628
|
+
"staleAfterMs",
|
|
629
|
+
),
|
|
384
630
|
};
|
|
385
631
|
const now = options.now ?? Date.now;
|
|
386
632
|
if (typeof now !== "function") {
|
|
@@ -445,12 +691,15 @@ export function createMonitorStore(options = {}) {
|
|
|
445
691
|
}
|
|
446
692
|
|
|
447
693
|
function getSnapshot() {
|
|
694
|
+
const snapshotAtMs = now();
|
|
448
695
|
return {
|
|
449
696
|
schema_version: 1,
|
|
450
697
|
source_of_truth: "hook",
|
|
451
698
|
updated_at_ms: updatedAtMs,
|
|
452
699
|
sessions: [...sessions.values()]
|
|
453
|
-
.map(
|
|
700
|
+
.map((session) =>
|
|
701
|
+
snapshotSession(session, snapshotAtMs, limits.staleAfterMs),
|
|
702
|
+
)
|
|
454
703
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
455
704
|
diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
456
705
|
};
|
|
@@ -11,6 +11,12 @@ const NORMALIZED_EVENT_TYPES = Object.freeze({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
const SESSION_EVENT_TYPES = new Set(["session_started", "session_ended"]);
|
|
14
|
+
const SESSION_START_SOURCES = new Set([
|
|
15
|
+
"startup",
|
|
16
|
+
"resume",
|
|
17
|
+
"clear",
|
|
18
|
+
"compact",
|
|
19
|
+
]);
|
|
14
20
|
|
|
15
21
|
const MAX_IDENTIFIER_LENGTH = 512;
|
|
16
22
|
const MAX_LABEL_LENGTH = 256;
|
|
@@ -189,6 +195,13 @@ export function normalizeHookPayload(payload, options = {}) {
|
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
const event = commonEvent(payload, type, receivedAtMs);
|
|
198
|
+
if (
|
|
199
|
+
type === "session_started" &&
|
|
200
|
+
typeof payload.source === "string" &&
|
|
201
|
+
SESSION_START_SOURCES.has(payload.source)
|
|
202
|
+
) {
|
|
203
|
+
event.session_start_source = payload.source;
|
|
204
|
+
}
|
|
192
205
|
const workspaceLabel = optionalWorkspaceLabel(payload);
|
|
193
206
|
if (workspaceLabel) {
|
|
194
207
|
event.workspace_label = workspaceLabel;
|