codex-agent-view 0.4.5 → 0.4.6
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 +230 -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.6` 사용법을 설명한다. **최초 설치만** 일반 터미널에서 아래 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.6
|
|
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.6`은 lifecycle 정확성을 바로잡는 릴리스다. `SessionEnd`에 최종 우선순위를 부여하고, 끝나지 않은 child activity를 성공으로 추정하지 않은 채 정리하며, 늦게 도착한 event가 완료된 turn을 다시 실행 중으로 열지 못하게 한다. 종료 신호 없이 오래 남은 활동은 무기한 **실행 중**으로 두지 않고 **종료 미확인**으로 표시한다. 아래의 기존 제품 구성은 그대로 유지한다.
|
|
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.6`을 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.6
|
|
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.6` 설치 경로는 위의 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.6`. 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.6
|
|
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.6` is the lifecycle-correctness release. It gives `SessionEnd` terminal priority, settles unfinished child activity without pretending it succeeded, prevents late events from reopening a finished turn, and reports prolonged unconfirmed activity as **End not confirmed** instead of leaving it indefinitely **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.6` 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.6
|
|
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.6` 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
|
}
|
|
@@ -125,11 +236,35 @@ function applySessionEvent(session, event, limits) {
|
|
|
125
236
|
const lifecycle = session.lifecycle;
|
|
126
237
|
if (event.type === "session_started") {
|
|
127
238
|
const resumedAfterEnd = lifecycle.end_observed;
|
|
239
|
+
const startsNewEpoch =
|
|
240
|
+
resumedAfterEnd ||
|
|
241
|
+
event.session_start_source === "resume" ||
|
|
242
|
+
event.session_start_source === "clear";
|
|
243
|
+
if (event.session_start_source === "compact") {
|
|
244
|
+
if (resumedAfterEnd) {
|
|
245
|
+
return "stale";
|
|
246
|
+
}
|
|
247
|
+
lifecycle.start_observed = true;
|
|
248
|
+
lifecycle.started_at_ms ??= event.received_at_ms;
|
|
249
|
+
addActivity(session, event, "observed", limits.maxActivitiesPerSession);
|
|
250
|
+
return "applied";
|
|
251
|
+
}
|
|
252
|
+
if (lifecycle.start_observed && !startsNewEpoch) {
|
|
253
|
+
return "duplicate";
|
|
254
|
+
}
|
|
255
|
+
if (startsNewEpoch) {
|
|
256
|
+
if (!resumedAfterEnd) {
|
|
257
|
+
settleRootTurnActivity(session, "completion_not_observed");
|
|
258
|
+
clearPermission(session, "completion_not_observed");
|
|
259
|
+
settleRunningState(session, "completion_not_observed");
|
|
260
|
+
}
|
|
261
|
+
resetTransientState(session);
|
|
262
|
+
}
|
|
128
263
|
lifecycle.start_observed = true;
|
|
129
|
-
lifecycle.started_at_ms
|
|
264
|
+
lifecycle.started_at_ms = event.received_at_ms;
|
|
130
265
|
lifecycle.end_observed = false;
|
|
131
266
|
lifecycle.ended_at_ms = null;
|
|
132
|
-
lifecycle.has_out_of_order_events
|
|
267
|
+
lifecycle.has_out_of_order_events = false;
|
|
133
268
|
addActivity(session, event, "observed", limits.maxActivitiesPerSession);
|
|
134
269
|
return "applied";
|
|
135
270
|
}
|
|
@@ -140,9 +275,11 @@ function applySessionEvent(session, event, limits) {
|
|
|
140
275
|
lifecycle.end_observed = true;
|
|
141
276
|
lifecycle.ended_at_ms = event.received_at_ms;
|
|
142
277
|
lifecycle.has_out_of_order_events = !lifecycle.start_observed;
|
|
278
|
+
settleRootTurnActivity(session, "completed");
|
|
143
279
|
session.root_turn.status = "completed";
|
|
144
280
|
session.root_turn.stopped_at_ms ??= event.received_at_ms;
|
|
145
|
-
session
|
|
281
|
+
clearPermission(session, "interrupted");
|
|
282
|
+
settleRunningState(session, "interrupted");
|
|
146
283
|
addActivity(session, event, "completed", limits.maxActivitiesPerSession);
|
|
147
284
|
return "applied";
|
|
148
285
|
}
|
|
@@ -150,9 +287,10 @@ function applySessionEvent(session, event, limits) {
|
|
|
150
287
|
function applyTurnEvent(session, event, limits) {
|
|
151
288
|
const turn = session.root_turn;
|
|
152
289
|
if (event.type === "turn_started") {
|
|
153
|
-
if (turn.turn_id === event.turn_id
|
|
154
|
-
return "duplicate";
|
|
290
|
+
if (turn.turn_id === event.turn_id) {
|
|
291
|
+
return turn.status === "running" ? "duplicate" : "stale";
|
|
155
292
|
}
|
|
293
|
+
settleRunningState(session, "completion_not_observed");
|
|
156
294
|
session.root_turn = {
|
|
157
295
|
status: "running",
|
|
158
296
|
turn_id: event.turn_id,
|
|
@@ -160,7 +298,12 @@ function applyTurnEvent(session, event, limits) {
|
|
|
160
298
|
stopped_at_ms: null,
|
|
161
299
|
has_out_of_order_events: false,
|
|
162
300
|
};
|
|
163
|
-
|
|
301
|
+
if (
|
|
302
|
+
session.permission.status !== "waiting_for_user" ||
|
|
303
|
+
session.permission.turn_id !== event.turn_id
|
|
304
|
+
) {
|
|
305
|
+
clearPermission(session, "completion_not_observed");
|
|
306
|
+
}
|
|
164
307
|
addActivity(session, event, "running", limits.maxActivitiesPerSession);
|
|
165
308
|
return "applied";
|
|
166
309
|
}
|
|
@@ -169,6 +312,9 @@ function applyTurnEvent(session, event, limits) {
|
|
|
169
312
|
return "duplicate";
|
|
170
313
|
}
|
|
171
314
|
const startObserved = turn.turn_id === event.turn_id && turn.started_at_ms !== null;
|
|
315
|
+
if (startObserved) {
|
|
316
|
+
settleRootTurnActivity(session, "completed");
|
|
317
|
+
}
|
|
172
318
|
session.root_turn = {
|
|
173
319
|
status: "completed",
|
|
174
320
|
turn_id: event.turn_id,
|
|
@@ -176,7 +322,15 @@ function applyTurnEvent(session, event, limits) {
|
|
|
176
322
|
stopped_at_ms: event.received_at_ms,
|
|
177
323
|
has_out_of_order_events: !startObserved,
|
|
178
324
|
};
|
|
179
|
-
|
|
325
|
+
if (
|
|
326
|
+
session.permission.status === "waiting_for_user" &&
|
|
327
|
+
session.permission.turn_id === event.turn_id
|
|
328
|
+
) {
|
|
329
|
+
clearPermission(session, "completion_not_observed");
|
|
330
|
+
}
|
|
331
|
+
settleRunningState(session, "completion_not_observed", {
|
|
332
|
+
turnId: event.turn_id,
|
|
333
|
+
});
|
|
180
334
|
addActivity(
|
|
181
335
|
session,
|
|
182
336
|
event,
|
|
@@ -192,6 +346,7 @@ function applySubagentEvent(session, event, limits) {
|
|
|
192
346
|
agent = {
|
|
193
347
|
agent_id: event.agent_id,
|
|
194
348
|
agent_type: event.agent_type,
|
|
349
|
+
turn_id: event.turn_id,
|
|
195
350
|
status: "unknown",
|
|
196
351
|
started_at_ms: null,
|
|
197
352
|
stopped_at_ms: null,
|
|
@@ -225,6 +380,7 @@ function applySubagentEvent(session, event, limits) {
|
|
|
225
380
|
}
|
|
226
381
|
|
|
227
382
|
agent.agent_type = event.agent_type;
|
|
383
|
+
agent.turn_id = event.turn_id;
|
|
228
384
|
agent.last_seen_at_ms = Math.max(agent.last_seen_at_ms, event.received_at_ms);
|
|
229
385
|
touchMapEntry(session.agents, event.agent_id, agent);
|
|
230
386
|
trimMap(session.agents, limits.maxAgentsPerSession);
|
|
@@ -282,7 +438,7 @@ function applyToolEvent(session, event, limits) {
|
|
|
282
438
|
permission.turn_id === event.turn_id &&
|
|
283
439
|
event.received_at_ms >= permission.requested_at_ms
|
|
284
440
|
) {
|
|
285
|
-
session
|
|
441
|
+
clearPermission(session, "completed");
|
|
286
442
|
}
|
|
287
443
|
}
|
|
288
444
|
|
|
@@ -328,9 +484,29 @@ function applyPermissionEvent(session, event, limits) {
|
|
|
328
484
|
}
|
|
329
485
|
|
|
330
486
|
function applyEvent(session, event, limits) {
|
|
331
|
-
if (event.type === "session_started"
|
|
487
|
+
if (event.type === "session_started") {
|
|
488
|
+
return applySessionEvent(session, event, limits);
|
|
489
|
+
}
|
|
490
|
+
if (
|
|
491
|
+
session.lifecycle.end_observed &&
|
|
492
|
+
event.type !== "tool_completed" &&
|
|
493
|
+
event.type !== "subagent_stopped"
|
|
494
|
+
) {
|
|
495
|
+
return event.type === "session_ended" ? "duplicate" : "stale";
|
|
496
|
+
}
|
|
497
|
+
if (event.type === "session_ended") {
|
|
332
498
|
return applySessionEvent(session, event, limits);
|
|
333
499
|
}
|
|
500
|
+
if (
|
|
501
|
+
session.root_turn.status === "completed" &&
|
|
502
|
+
session.root_turn.turn_id === event.turn_id &&
|
|
503
|
+
(event.type === "turn_started" ||
|
|
504
|
+
event.type === "permission_requested" ||
|
|
505
|
+
event.type === "tool_started" ||
|
|
506
|
+
event.type === "subagent_started")
|
|
507
|
+
) {
|
|
508
|
+
return "stale";
|
|
509
|
+
}
|
|
334
510
|
if (event.type === "turn_started" || event.type === "turn_stopped") {
|
|
335
511
|
return applyTurnEvent(session, event, limits);
|
|
336
512
|
}
|
|
@@ -343,22 +519,51 @@ function applyEvent(session, event, limits) {
|
|
|
343
519
|
return applyPermissionEvent(session, event, limits);
|
|
344
520
|
}
|
|
345
521
|
|
|
346
|
-
function snapshotSession(session) {
|
|
522
|
+
function snapshotSession(session, nowMs, staleAfterMs) {
|
|
523
|
+
const staleActive = isStaleActiveSession(session, nowMs, staleAfterMs);
|
|
347
524
|
return {
|
|
348
525
|
session_id: session.session_id,
|
|
349
526
|
workspace_label: session.workspace_label,
|
|
350
527
|
task_summary: session.task_summary,
|
|
351
|
-
status: deriveSessionStatus(session),
|
|
528
|
+
status: deriveSessionStatus(session, nowMs, staleAfterMs),
|
|
352
529
|
first_seen_at_ms: session.first_seen_at_ms,
|
|
353
530
|
last_seen_at_ms: session.last_seen_at_ms,
|
|
354
531
|
agents: [...session.agents.values()]
|
|
355
|
-
.map(({ start_observed, stop_observed, ...agent }) => ({
|
|
532
|
+
.map(({ start_observed, stop_observed, ...agent }) => ({
|
|
533
|
+
...agent,
|
|
534
|
+
...(staleActive && agent.status === "running"
|
|
535
|
+
? { status: "completion_not_observed" }
|
|
536
|
+
: {}),
|
|
537
|
+
}))
|
|
356
538
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
357
|
-
|
|
539
|
+
tools: [...session.tools.values()]
|
|
540
|
+
.map(({ start_observed, completion_observed, ...tool }) => ({
|
|
541
|
+
...tool,
|
|
542
|
+
...(staleActive && tool.status === "running"
|
|
543
|
+
? { status: "completion_not_observed" }
|
|
544
|
+
: {}),
|
|
545
|
+
}))
|
|
546
|
+
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
547
|
+
root_turn: {
|
|
548
|
+
...session.root_turn,
|
|
549
|
+
...(staleActive && session.root_turn.status === "running"
|
|
550
|
+
? { status: "completion_not_observed" }
|
|
551
|
+
: {}),
|
|
552
|
+
},
|
|
358
553
|
recent_activities: session.recent_activities.map((activity) => ({
|
|
359
554
|
...activity,
|
|
555
|
+
...(staleActive &&
|
|
556
|
+
(activity.status === "running" ||
|
|
557
|
+
activity.status === "waiting_for_user")
|
|
558
|
+
? { status: "completion_not_observed" }
|
|
559
|
+
: {}),
|
|
360
560
|
})),
|
|
361
|
-
permission: {
|
|
561
|
+
permission: {
|
|
562
|
+
...session.permission,
|
|
563
|
+
...(staleActive && session.permission.status === "waiting_for_user"
|
|
564
|
+
? { status: "completion_not_observed" }
|
|
565
|
+
: {}),
|
|
566
|
+
},
|
|
362
567
|
};
|
|
363
568
|
}
|
|
364
569
|
|
|
@@ -381,6 +586,10 @@ export function createMonitorStore(options = {}) {
|
|
|
381
586
|
options.maxSessions ?? DEFAULT_LIMITS.maxSessions,
|
|
382
587
|
"maxSessions",
|
|
383
588
|
),
|
|
589
|
+
staleAfterMs: positiveInteger(
|
|
590
|
+
options.staleAfterMs ?? DEFAULT_LIMITS.staleAfterMs,
|
|
591
|
+
"staleAfterMs",
|
|
592
|
+
),
|
|
384
593
|
};
|
|
385
594
|
const now = options.now ?? Date.now;
|
|
386
595
|
if (typeof now !== "function") {
|
|
@@ -445,12 +654,15 @@ export function createMonitorStore(options = {}) {
|
|
|
445
654
|
}
|
|
446
655
|
|
|
447
656
|
function getSnapshot() {
|
|
657
|
+
const snapshotAtMs = now();
|
|
448
658
|
return {
|
|
449
659
|
schema_version: 1,
|
|
450
660
|
source_of_truth: "hook",
|
|
451
661
|
updated_at_ms: updatedAtMs,
|
|
452
662
|
sessions: [...sessions.values()]
|
|
453
|
-
.map(
|
|
663
|
+
.map((session) =>
|
|
664
|
+
snapshotSession(session, snapshotAtMs, limits.staleAfterMs),
|
|
665
|
+
)
|
|
454
666
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
455
667
|
diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
456
668
|
};
|
|
@@ -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;
|