mishkan-harness 0.2.5 → 0.2.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/README.md +1 -1
- package/package.json +1 -1
- package/payload/mishkan/hooks/post-tool-observe.sh +55 -7
- package/payload/mishkan/hooks/pre-tool-trace.sh +40 -6
- package/payload/mishkan/observability/README.md +15 -12
- package/payload/mishkan/observability/schema.json +18 -1
- package/payload/mishkan/observability/watch/pyproject.toml +1 -1
- package/payload/mishkan/observability/watch/src/mishkan_watch/__init__.py +1 -1
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/agents.py +29 -7
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/knowledge.py +7 -3
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/live.py +42 -13
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/usage.py +10 -3
- package/payload/mishkan/observability/watch/src/mishkan_watch/tabs/workflows.py +25 -6
- package/payload/mishkan/observability/watchd/pyproject.toml +1 -1
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/__init__.py +1 -1
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/__main__.py +52 -4
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/server.py +24 -1
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/sources/graphify_tail.py +38 -64
- package/payload/mishkan/observability/watchd/src/mishkan_watchd/state.py +222 -34
- package/payload/mishkan/observability/watchd/tests/test_state.py +502 -4
- package/payload/mishkan/scripts/mishkan-ingest.sh +4 -0
|
@@ -10,6 +10,7 @@ from mishkan_watchd.state import HarnessState
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def test_agent_spawn_creates_session_and_agent():
|
|
13
|
+
"""tool_use_id is the key; display name is preserved in AgentState.name."""
|
|
13
14
|
s = HarnessState()
|
|
14
15
|
s.apply({"ts": "x", "session": "sess-1", "type": "session_start", "project": "/tmp/proj"})
|
|
15
16
|
s.apply({
|
|
@@ -19,11 +20,50 @@ def test_agent_spawn_creates_session_and_agent():
|
|
|
19
20
|
"type": "agent_spawn",
|
|
20
21
|
"tool": "Task",
|
|
21
22
|
"agent": "bezalel",
|
|
22
|
-
"payload": {"subagent_type": "bezalel"},
|
|
23
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "toolu_abc123"},
|
|
23
24
|
})
|
|
24
25
|
assert "sess-1" in s.sessions
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
agents = s.sessions["sess-1"].agents_active
|
|
27
|
+
# Keyed by tool_use_id, not bare agent name.
|
|
28
|
+
assert "toolu_abc123" in agents
|
|
29
|
+
assert agents["toolu_abc123"].name == "bezalel"
|
|
30
|
+
assert agents["toolu_abc123"].status == "running"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_agent_spawn_complete_pair_by_tool_use_id():
|
|
34
|
+
"""spawn + complete keyed by tool_use_id correctly decrements active count."""
|
|
35
|
+
s = HarnessState()
|
|
36
|
+
s.apply({"ts": "x", "session": "s", "type": "session_start", "project": "/p"})
|
|
37
|
+
# Spawn two concurrent agents with the same subagent_type.
|
|
38
|
+
for tid in ("tid-1", "tid-2"):
|
|
39
|
+
s.apply({
|
|
40
|
+
"ts": "x", "session": "s", "type": "agent_spawn", "tool": "Task",
|
|
41
|
+
"agent": "bezalel",
|
|
42
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": tid},
|
|
43
|
+
})
|
|
44
|
+
assert len(s.sessions["s"].agents_active) == 2
|
|
45
|
+
# Complete the first one.
|
|
46
|
+
s.apply({
|
|
47
|
+
"ts": "y", "session": "s", "type": "agent_complete", "tool": "Task",
|
|
48
|
+
"agent": "bezalel",
|
|
49
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "tid-1"},
|
|
50
|
+
})
|
|
51
|
+
agents = s.sessions["s"].agents_active
|
|
52
|
+
assert "tid-1" not in agents
|
|
53
|
+
assert "tid-2" in agents
|
|
54
|
+
assert len(agents) == 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_agent_spawn_legacy_fallback_no_tool_use_id():
|
|
58
|
+
"""Legacy events without tool_use_id still key by agent name."""
|
|
59
|
+
s = HarnessState()
|
|
60
|
+
s.apply({"ts": "x", "session": "s", "type": "session_start", "project": "/p"})
|
|
61
|
+
s.apply({
|
|
62
|
+
"ts": "x", "session": "s", "type": "agent_spawn", "tool": "Task",
|
|
63
|
+
"agent": "caleb",
|
|
64
|
+
"payload": {"subagent_type": "caleb"},
|
|
65
|
+
})
|
|
66
|
+
assert "caleb" in s.sessions["s"].agents_active
|
|
27
67
|
|
|
28
68
|
|
|
29
69
|
def test_token_usage_accumulates_per_session():
|
|
@@ -43,9 +83,15 @@ def test_token_usage_accumulates_per_session():
|
|
|
43
83
|
|
|
44
84
|
|
|
45
85
|
def test_session_stop_removes_session():
|
|
86
|
+
"""An idle session (no agents, last_event_mono expired) is cleaned up on session_stop."""
|
|
87
|
+
import mishkan_watchd.state as state_mod
|
|
88
|
+
from time import monotonic
|
|
89
|
+
|
|
46
90
|
s = HarnessState()
|
|
47
91
|
s.apply({"ts": "x", "session": "drop-me", "type": "session_start", "project": "/p"})
|
|
48
92
|
assert "drop-me" in s.sessions
|
|
93
|
+
# Simulate idleness: backdate last_event_mono past the keepalive window.
|
|
94
|
+
s.sessions["drop-me"].last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
49
95
|
s.apply({"ts": "y", "session": "drop-me", "type": "session_stop"})
|
|
50
96
|
assert "drop-me" not in s.sessions
|
|
51
97
|
|
|
@@ -57,13 +103,55 @@ def test_snapshot_serializes_cleanly():
|
|
|
57
103
|
s.apply({
|
|
58
104
|
"ts": "x", "session": "snap", "project": "/p",
|
|
59
105
|
"type": "agent_spawn", "tool": "Task", "agent": "caleb",
|
|
60
|
-
"payload": {"subagent_type": "caleb"},
|
|
106
|
+
"payload": {"subagent_type": "caleb", "tool_use_id": "toolu_snap1"},
|
|
61
107
|
})
|
|
62
108
|
snap = s.to_snapshot()
|
|
109
|
+
# Agent is present and keyed by tool_use_id.
|
|
110
|
+
assert "toolu_snap1" in snap["sessions"]["snap"]["agents_active"]
|
|
63
111
|
# Round-trips through JSON without error.
|
|
64
112
|
json.dumps(snap, default=str)
|
|
65
113
|
|
|
66
114
|
|
|
115
|
+
def test_graphify_hook_event_increments_scan_count():
|
|
116
|
+
"""graphify_scan from the Bash hook (no stats_only) increments scans."""
|
|
117
|
+
s = HarnessState()
|
|
118
|
+
assert s.graphify.scans == 0
|
|
119
|
+
s.apply({
|
|
120
|
+
"ts": "x", "session": None, "type": "graphify_scan",
|
|
121
|
+
"payload": {"project": "/p", "nodes": 100, "edges": 200},
|
|
122
|
+
})
|
|
123
|
+
assert s.graphify.scans == 1
|
|
124
|
+
assert s.graphify.nodes == 100
|
|
125
|
+
assert s.graphify.edges == 200
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_graphify_tail_stats_only_does_not_increment_scan_count():
|
|
129
|
+
"""graphify_scan with stats_only=True updates sizes but not the counter."""
|
|
130
|
+
s = HarnessState()
|
|
131
|
+
s.apply({
|
|
132
|
+
"ts": "x", "session": None, "type": "graphify_scan",
|
|
133
|
+
"payload": {
|
|
134
|
+
"project": "/p", "nodes": 2798, "edges": 3102,
|
|
135
|
+
"stats_only": True,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
assert s.graphify.scans == 0
|
|
139
|
+
assert s.graphify.nodes == 2798
|
|
140
|
+
assert s.graphify.edges == 3102
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_graphify_query_hook_event_increments_query_count():
|
|
144
|
+
"""graphify_query from the Bash hook increments queries."""
|
|
145
|
+
s = HarnessState()
|
|
146
|
+
s.apply({"ts": "x", "session": "sq", "type": "session_start", "project": "/p"})
|
|
147
|
+
s.apply({
|
|
148
|
+
"ts": "x", "session": "sq", "type": "graphify_query",
|
|
149
|
+
"payload": {"project": "/p", "question": "who calls process_payment"},
|
|
150
|
+
})
|
|
151
|
+
assert s.graphify.queries == 1
|
|
152
|
+
assert s.graphify.last_query_text == "who calls process_payment"
|
|
153
|
+
|
|
154
|
+
|
|
67
155
|
def test_unknown_event_type_is_ignored_gracefully():
|
|
68
156
|
s = HarnessState()
|
|
69
157
|
s.apply({"ts": "x", "session": "weird", "type": "session_start", "project": "/p"})
|
|
@@ -72,10 +160,420 @@ def test_unknown_event_type_is_ignored_gracefully():
|
|
|
72
160
|
assert "weird" in s.sessions
|
|
73
161
|
|
|
74
162
|
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Fix A — last_context_tokens
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def test_last_context_tokens_set_not_accumulated():
|
|
168
|
+
"""last_context_tokens reflects the MOST RECENT turn, not a running sum."""
|
|
169
|
+
s = HarnessState()
|
|
170
|
+
s.apply({"ts": "x", "session": "ctx", "type": "session_start", "project": "/p"})
|
|
171
|
+
# Turn 1: small uncached input with large cache_read (typical caching profile).
|
|
172
|
+
s.apply({
|
|
173
|
+
"ts": "x", "session": "ctx", "type": "token_usage",
|
|
174
|
+
"payload": {
|
|
175
|
+
"tokens_in": 2, "tokens_out": 100,
|
|
176
|
+
"cache_read": 558453, "cache_write": 5141,
|
|
177
|
+
"cost_estimate_usd": 1.21,
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
sess = s.sessions["ctx"]
|
|
181
|
+
# last_context_tokens = cache_read + cache_write + tokens_in of THIS turn.
|
|
182
|
+
assert sess.last_context_tokens == 558453 + 5141 + 2
|
|
183
|
+
# Cumulative fields are unchanged.
|
|
184
|
+
assert sess.tokens_in == 2
|
|
185
|
+
assert sess.cache_read == 558453
|
|
186
|
+
|
|
187
|
+
# Turn 2: a different footprint — last_context_tokens is replaced, not added.
|
|
188
|
+
s.apply({
|
|
189
|
+
"ts": "y", "session": "ctx", "type": "token_usage",
|
|
190
|
+
"payload": {
|
|
191
|
+
"tokens_in": 5, "tokens_out": 200,
|
|
192
|
+
"cache_read": 600000, "cache_write": 0,
|
|
193
|
+
"cost_estimate_usd": 0.5,
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
assert sess.last_context_tokens == 600000 + 0 + 5
|
|
197
|
+
# Cumulative tokens_in did accumulate across both turns.
|
|
198
|
+
assert sess.tokens_in == 7
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_last_context_tokens_in_snapshot():
|
|
202
|
+
"""last_context_tokens is present in the per-session snapshot dict."""
|
|
203
|
+
import json
|
|
204
|
+
s = HarnessState()
|
|
205
|
+
s.apply({"ts": "x", "session": "ctx2", "type": "session_start", "project": "/p"})
|
|
206
|
+
s.apply({
|
|
207
|
+
"ts": "x", "session": "ctx2", "type": "token_usage",
|
|
208
|
+
"payload": {"tokens_in": 2, "tokens_out": 50,
|
|
209
|
+
"cache_read": 558453, "cache_write": 5141,
|
|
210
|
+
"cost_estimate_usd": 0.1},
|
|
211
|
+
})
|
|
212
|
+
snap = s.to_snapshot()
|
|
213
|
+
sess_snap = snap["sessions"]["ctx2"]
|
|
214
|
+
assert "last_context_tokens" in sess_snap
|
|
215
|
+
assert sess_snap["last_context_tokens"] == 558453 + 5141 + 2
|
|
216
|
+
# Verify it round-trips cleanly.
|
|
217
|
+
json.dumps(snap, default=str)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_last_context_tokens_zero_on_fresh_session():
|
|
221
|
+
"""A session with no token_usage events has last_context_tokens == 0."""
|
|
222
|
+
s = HarnessState()
|
|
223
|
+
s.apply({"ts": "x", "session": "fresh", "type": "session_start", "project": "/p"})
|
|
224
|
+
snap = s.to_snapshot()
|
|
225
|
+
assert snap["sessions"]["fresh"]["last_context_tokens"] == 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Fix B — workflow stale sweep
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def test_workflow_start_creates_run_in_session():
|
|
233
|
+
"""workflow_start populates workflows_active with phase 'running'."""
|
|
234
|
+
s = HarnessState()
|
|
235
|
+
s.apply({"ts": "x", "session": "wf1", "type": "session_start", "project": "/p"})
|
|
236
|
+
s.apply({
|
|
237
|
+
"ts": "x", "session": "wf1", "type": "workflow_start",
|
|
238
|
+
"payload": {"name": "my-flow", "run_id": "run-abc", "scriptPath": "flow.yaml"},
|
|
239
|
+
})
|
|
240
|
+
wf = s.sessions["wf1"].workflows_active.get("run-abc")
|
|
241
|
+
assert wf is not None
|
|
242
|
+
assert wf.name == "my-flow"
|
|
243
|
+
assert wf.phase == "running"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_workflow_stale_sweep_in_snapshot():
|
|
247
|
+
"""A workflow whose last_activity_mono is > TTL shows phase='stale' in snapshot."""
|
|
248
|
+
import mishkan_watchd.state as state_mod
|
|
249
|
+
from time import monotonic
|
|
250
|
+
|
|
251
|
+
s = HarnessState()
|
|
252
|
+
s.apply({"ts": "x", "session": "wf2", "type": "session_start", "project": "/p"})
|
|
253
|
+
s.apply({
|
|
254
|
+
"ts": "x", "session": "wf2", "type": "workflow_start",
|
|
255
|
+
"payload": {"name": "old-flow", "run_id": "run-old"},
|
|
256
|
+
})
|
|
257
|
+
# Backdate last_activity_mono to simulate a run that started long ago.
|
|
258
|
+
wf = s.sessions["wf2"].workflows_active["run-old"]
|
|
259
|
+
wf.last_activity_mono = monotonic() - (state_mod.WORKFLOW_STALE_TTL_S + 1)
|
|
260
|
+
|
|
261
|
+
snap = s.to_snapshot()
|
|
262
|
+
wf_snap = snap["sessions"]["wf2"]["workflows_active"]["run-old"]
|
|
263
|
+
assert wf_snap["phase"] == "stale"
|
|
264
|
+
# The live WorkflowState is NOT mutated — only the snapshot output changes.
|
|
265
|
+
assert wf.phase == "running"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_workflow_fresh_run_not_staled():
|
|
269
|
+
"""A recently-started workflow is NOT staled in the snapshot."""
|
|
270
|
+
s = HarnessState()
|
|
271
|
+
s.apply({"ts": "x", "session": "wf3", "type": "session_start", "project": "/p"})
|
|
272
|
+
s.apply({
|
|
273
|
+
"ts": "x", "session": "wf3", "type": "workflow_start",
|
|
274
|
+
"payload": {"name": "new-flow", "run_id": "run-new"},
|
|
275
|
+
})
|
|
276
|
+
snap = s.to_snapshot()
|
|
277
|
+
wf_snap = snap["sessions"]["wf3"]["workflows_active"]["run-new"]
|
|
278
|
+
assert wf_snap["phase"] == "running"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_workflow_stale_field_not_in_snapshot():
|
|
282
|
+
"""last_activity_mono (internal float) is stripped from the snapshot wire shape."""
|
|
283
|
+
s = HarnessState()
|
|
284
|
+
s.apply({"ts": "x", "session": "wf4", "type": "session_start", "project": "/p"})
|
|
285
|
+
s.apply({
|
|
286
|
+
"ts": "x", "session": "wf4", "type": "workflow_start",
|
|
287
|
+
"payload": {"name": "flow", "run_id": "run-x"},
|
|
288
|
+
})
|
|
289
|
+
snap = s.to_snapshot()
|
|
290
|
+
wf_snap = snap["sessions"]["wf4"]["workflows_active"]["run-x"]
|
|
291
|
+
assert "last_activity_mono" not in wf_snap
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_cold_start_has_no_workflows():
|
|
295
|
+
"""A fresh daemon with no workflow events shows an empty workflows_active."""
|
|
296
|
+
s = HarnessState()
|
|
297
|
+
s.apply({"ts": "x", "session": "cold", "type": "session_start", "project": "/p"})
|
|
298
|
+
snap = s.to_snapshot()
|
|
299
|
+
assert snap["sessions"]["cold"]["workflows_active"] == {}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Fix C — session_stop busy-guard (liveness via bus activity)
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
def test_session_stop_ignored_when_agent_active():
|
|
307
|
+
"""A session with a running agent survives session_stop from session_discover."""
|
|
308
|
+
s = HarnessState()
|
|
309
|
+
s.apply({"ts": "x", "session": "busy-1", "type": "session_start", "project": "/p"})
|
|
310
|
+
s.apply({
|
|
311
|
+
"ts": "x", "session": "busy-1", "type": "agent_spawn", "tool": "Task",
|
|
312
|
+
"agent": "bezalel",
|
|
313
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "toolu_busy1"},
|
|
314
|
+
})
|
|
315
|
+
# Transcript goes quiet — session_discover emits session_stop.
|
|
316
|
+
s.apply({"ts": "y", "session": "busy-1", "type": "session_stop"})
|
|
317
|
+
# Session must still be present and agent still visible.
|
|
318
|
+
assert "busy-1" in s.sessions
|
|
319
|
+
assert "toolu_busy1" in s.sessions["busy-1"].agents_active
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_session_stop_ignored_when_recent_bus_event():
|
|
323
|
+
"""A session whose last bus event is within SESSION_KEEPALIVE_S survives session_stop."""
|
|
324
|
+
import mishkan_watchd.state as state_mod
|
|
325
|
+
|
|
326
|
+
s = HarnessState()
|
|
327
|
+
s.apply({"ts": "x", "session": "busy-2", "type": "session_start", "project": "/p"})
|
|
328
|
+
# A recent token_usage event stamps last_event_mono close to now.
|
|
329
|
+
s.apply({
|
|
330
|
+
"ts": "x", "session": "busy-2", "type": "token_usage",
|
|
331
|
+
"payload": {"tokens_in": 10, "tokens_out": 5, "cost_estimate_usd": 0.001},
|
|
332
|
+
})
|
|
333
|
+
# Confirm last_event_mono is within keepalive window.
|
|
334
|
+
from time import monotonic
|
|
335
|
+
assert (monotonic() - s.sessions["busy-2"].last_event_mono) < state_mod.SESSION_KEEPALIVE_S
|
|
336
|
+
|
|
337
|
+
s.apply({"ts": "y", "session": "busy-2", "type": "session_stop"})
|
|
338
|
+
assert "busy-2" in s.sessions
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_session_stop_drops_genuinely_idle_session():
|
|
342
|
+
"""A session with no active agents and an old last_event_mono is cleaned up normally."""
|
|
343
|
+
import mishkan_watchd.state as state_mod
|
|
344
|
+
from time import monotonic
|
|
345
|
+
|
|
346
|
+
s = HarnessState()
|
|
347
|
+
s.apply({"ts": "x", "session": "idle-1", "type": "session_start", "project": "/p"})
|
|
348
|
+
# Backdate last_event_mono to simulate a long-idle session.
|
|
349
|
+
sess = s.sessions["idle-1"]
|
|
350
|
+
sess.last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
351
|
+
# No agents_active (default empty dict).
|
|
352
|
+
s.apply({"ts": "y", "session": "idle-1", "type": "session_stop"})
|
|
353
|
+
assert "idle-1" not in s.sessions
|
|
354
|
+
assert "idle-1" in s._stopped_recently
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_live_event_after_idle_stop_resurrects_session():
|
|
358
|
+
"""A live bus event for a tombstoned-but-spuriously-stopped session
|
|
359
|
+
un-tombstones and re-confirms the session (Fix E).
|
|
360
|
+
|
|
361
|
+
agent_complete for a session with no spawned agents is a harmless
|
|
362
|
+
fall-through: the session exists but agents_active stays empty. This
|
|
363
|
+
is correct — the event was genuine activity, not a phantom replay.
|
|
364
|
+
"""
|
|
365
|
+
import mishkan_watchd.state as state_mod
|
|
366
|
+
from time import monotonic
|
|
367
|
+
|
|
368
|
+
s = HarnessState()
|
|
369
|
+
s.apply({"ts": "x", "session": "gone-1", "type": "session_start", "project": "/p"})
|
|
370
|
+
sess = s.sessions["gone-1"]
|
|
371
|
+
sess.last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
372
|
+
s.apply({"ts": "y", "session": "gone-1", "type": "session_stop"})
|
|
373
|
+
assert "gone-1" not in s.sessions
|
|
374
|
+
assert "gone-1" in s._stopped_recently
|
|
375
|
+
|
|
376
|
+
# A live agent_complete arrives — session is resurrected (no agent to pop,
|
|
377
|
+
# but the session itself is re-confirmed and alive).
|
|
378
|
+
s.apply({
|
|
379
|
+
"ts": "z", "session": "gone-1", "type": "agent_complete",
|
|
380
|
+
"payload": {"tool_use_id": "toolu_late"},
|
|
381
|
+
})
|
|
382
|
+
assert "gone-1" in s.sessions
|
|
383
|
+
assert "gone-1" in s._confirmed_alive
|
|
384
|
+
assert "gone-1" not in s._stopped_recently
|
|
385
|
+
# No agent was ever spawned, so agents_active is empty — not an error.
|
|
386
|
+
assert s.sessions["gone-1"].agents_active == {}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
# Fix D — live-event confirm (agent-only session, no prior session_start)
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
def test_agent_spawn_alone_confirms_session_and_populates_agents_active():
|
|
394
|
+
"""Core regression: agent_spawn for a never-seen session (no session_start)
|
|
395
|
+
must create+confirm the session and populate agents_active immediately.
|
|
396
|
+
|
|
397
|
+
This is the agent-only-session blindspot: the parent transcript is quiet
|
|
398
|
+
during an agent run, so session_discover never fires session_start; but
|
|
399
|
+
bus events stream in live and tier-2 confirmation must pick them up.
|
|
400
|
+
"""
|
|
401
|
+
s = HarnessState()
|
|
402
|
+
s.apply({
|
|
403
|
+
"ts": "2026-06-09T10:00:00.000Z",
|
|
404
|
+
"session": "agent-only-sess",
|
|
405
|
+
"project": "/tmp/wisemoney",
|
|
406
|
+
"type": "agent_spawn",
|
|
407
|
+
"tool": "Task",
|
|
408
|
+
"agent": "bezalel",
|
|
409
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "toolu_live01"},
|
|
410
|
+
})
|
|
411
|
+
# Session must exist — created by tier-2 confirm.
|
|
412
|
+
assert "agent-only-sess" in s.sessions
|
|
413
|
+
# Agent must be visible immediately — event fell through to dispatch.
|
|
414
|
+
agents = s.sessions["agent-only-sess"].agents_active
|
|
415
|
+
assert "toolu_live01" in agents
|
|
416
|
+
assert agents["toolu_live01"].name == "bezalel"
|
|
417
|
+
assert agents["toolu_live01"].status == "running"
|
|
418
|
+
# Session is now confirmed.
|
|
419
|
+
assert "agent-only-sess" in s._confirmed_alive
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def test_token_usage_alone_confirms_session_and_applies_tokens():
|
|
423
|
+
"""token_usage for a never-seen session creates the session and lands tokens."""
|
|
424
|
+
s = HarnessState()
|
|
425
|
+
s.apply({
|
|
426
|
+
"ts": "2026-06-09T10:00:01.000Z",
|
|
427
|
+
"session": "tok-only-sess",
|
|
428
|
+
"type": "token_usage",
|
|
429
|
+
"payload": {"tokens_in": 42, "tokens_out": 7, "cost_estimate_usd": 0.005},
|
|
430
|
+
})
|
|
431
|
+
assert "tok-only-sess" in s.sessions
|
|
432
|
+
sess = s.sessions["tok-only-sess"]
|
|
433
|
+
assert sess.tokens_in == 42
|
|
434
|
+
assert sess.tokens_out == 7
|
|
435
|
+
assert "tok-only-sess" in s._confirmed_alive
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_tombstoned_session_untombstoned_on_live_agent_spawn():
|
|
439
|
+
"""Core regression — Fix E: a tombstoned session that receives a live
|
|
440
|
+
agent_spawn is un-tombstoned, re-confirmed, and the agent appears in
|
|
441
|
+
agents_active immediately.
|
|
442
|
+
|
|
443
|
+
Scenario: session went quiet (parent transcript stale >60 s during an
|
|
444
|
+
agent run), session_discover spuriously emitted session_stop, session was
|
|
445
|
+
tombstoned. A fresh agent_spawn then arrives on the bus. With the fix the
|
|
446
|
+
session must come back alive and the agent must be visible.
|
|
447
|
+
"""
|
|
448
|
+
import mishkan_watchd.state as state_mod
|
|
449
|
+
from time import monotonic
|
|
450
|
+
|
|
451
|
+
s = HarnessState()
|
|
452
|
+
# Start and let it go idle so the stop is accepted (not busy-guarded).
|
|
453
|
+
s.apply({"ts": "x", "session": "quiet-sess", "type": "session_start", "project": "/p"})
|
|
454
|
+
s.sessions["quiet-sess"].last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
455
|
+
s.apply({"ts": "y", "session": "quiet-sess", "type": "session_stop"})
|
|
456
|
+
assert "quiet-sess" not in s.sessions
|
|
457
|
+
assert "quiet-sess" in s._stopped_recently
|
|
458
|
+
|
|
459
|
+
# Live agent_spawn arrives — session must be resurrected.
|
|
460
|
+
s.apply({
|
|
461
|
+
"ts": "z", "session": "quiet-sess",
|
|
462
|
+
"type": "agent_spawn", "tool": "Task", "agent": "bezalel",
|
|
463
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "toolu_reborn"},
|
|
464
|
+
})
|
|
465
|
+
assert "quiet-sess" in s.sessions, "session must be un-tombstoned and re-confirmed"
|
|
466
|
+
assert "quiet-sess" in s._confirmed_alive
|
|
467
|
+
assert "quiet-sess" not in s._stopped_recently
|
|
468
|
+
agents = s.sessions["quiet-sess"].agents_active
|
|
469
|
+
assert "toolu_reborn" in agents, "agent must appear in agents_active after resurrection"
|
|
470
|
+
assert agents["toolu_reborn"].name == "bezalel"
|
|
471
|
+
assert agents["toolu_reborn"].status == "running"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def test_tombstoned_session_subsequent_agent_complete_works_normally():
|
|
475
|
+
"""After resurrection via agent_spawn a following agent_complete removes
|
|
476
|
+
the agent normally — the un-tombstoned session behaves like any live session.
|
|
477
|
+
"""
|
|
478
|
+
import mishkan_watchd.state as state_mod
|
|
479
|
+
from time import monotonic
|
|
480
|
+
|
|
481
|
+
s = HarnessState()
|
|
482
|
+
s.apply({"ts": "x", "session": "revived", "type": "session_start", "project": "/p"})
|
|
483
|
+
s.sessions["revived"].last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
484
|
+
s.apply({"ts": "y", "session": "revived", "type": "session_stop"})
|
|
485
|
+
assert "revived" in s._stopped_recently
|
|
486
|
+
|
|
487
|
+
# Resurrect via spawn.
|
|
488
|
+
s.apply({
|
|
489
|
+
"ts": "z", "session": "revived",
|
|
490
|
+
"type": "agent_spawn", "tool": "Task", "agent": "caleb",
|
|
491
|
+
"payload": {"subagent_type": "caleb", "tool_use_id": "toolu_rev1"},
|
|
492
|
+
})
|
|
493
|
+
assert "toolu_rev1" in s.sessions["revived"].agents_active
|
|
494
|
+
|
|
495
|
+
# Complete the agent — must be removed cleanly.
|
|
496
|
+
s.apply({
|
|
497
|
+
"ts": "w", "session": "revived",
|
|
498
|
+
"type": "agent_complete", "tool": "Task", "agent": "caleb",
|
|
499
|
+
"payload": {"subagent_type": "caleb", "tool_use_id": "toolu_rev1"},
|
|
500
|
+
})
|
|
501
|
+
assert "toolu_rev1" not in s.sessions["revived"].agents_active
|
|
502
|
+
assert len(s.sessions["revived"].agents_active) == 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_tombstoned_session_with_no_further_events_stays_gone():
|
|
506
|
+
"""A session that is tombstoned and receives no further events is never
|
|
507
|
+
resurrected — tombstone stays in place indefinitely.
|
|
508
|
+
"""
|
|
509
|
+
import mishkan_watchd.state as state_mod
|
|
510
|
+
from time import monotonic
|
|
511
|
+
|
|
512
|
+
s = HarnessState()
|
|
513
|
+
s.apply({"ts": "x", "session": "truly-dead", "type": "session_start", "project": "/p"})
|
|
514
|
+
s.sessions["truly-dead"].last_event_mono = monotonic() - (state_mod.SESSION_KEEPALIVE_S + 10)
|
|
515
|
+
s.apply({"ts": "y", "session": "truly-dead", "type": "session_stop"})
|
|
516
|
+
|
|
517
|
+
# No further events arrive for this session. It must remain absent.
|
|
518
|
+
assert "truly-dead" not in s.sessions
|
|
519
|
+
assert "truly-dead" in s._stopped_recently
|
|
520
|
+
assert "truly-dead" not in s._confirmed_alive
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def test_session_start_after_live_confirm_is_harmless_reconfirm():
|
|
524
|
+
"""session_start arriving after tier-2 already confirmed the session is a
|
|
525
|
+
harmless re-confirm: no duplicate session created, project upgraded if needed.
|
|
526
|
+
"""
|
|
527
|
+
s = HarnessState()
|
|
528
|
+
# Tier-2 confirm via agent_spawn (project unknown at this point).
|
|
529
|
+
s.apply({
|
|
530
|
+
"ts": "a", "session": "reconfirm-sess",
|
|
531
|
+
"type": "agent_spawn", "tool": "Task", "agent": "bezalel",
|
|
532
|
+
"payload": {"subagent_type": "bezalel", "tool_use_id": "toolu_rc1"},
|
|
533
|
+
})
|
|
534
|
+
assert "reconfirm-sess" in s.sessions
|
|
535
|
+
assert s.sessions["reconfirm-sess"].project == "unknown"
|
|
536
|
+
|
|
537
|
+
# session_discover fires session_start with the real project path.
|
|
538
|
+
s.apply({
|
|
539
|
+
"ts": "b", "session": "reconfirm-sess",
|
|
540
|
+
"type": "session_start", "project": "/real/project",
|
|
541
|
+
})
|
|
542
|
+
# Only one session entry — not duplicated.
|
|
543
|
+
assert len([sid for sid in s.sessions if sid == "reconfirm-sess"]) == 1
|
|
544
|
+
# Project upgraded from "unknown" to the real path.
|
|
545
|
+
assert s.sessions["reconfirm-sess"].project == "/real/project"
|
|
546
|
+
# Agent still present.
|
|
547
|
+
assert "toolu_rc1" in s.sessions["reconfirm-sess"].agents_active
|
|
548
|
+
|
|
549
|
+
|
|
75
550
|
if __name__ == "__main__":
|
|
76
551
|
test_agent_spawn_creates_session_and_agent()
|
|
552
|
+
test_agent_spawn_complete_pair_by_tool_use_id()
|
|
553
|
+
test_agent_spawn_legacy_fallback_no_tool_use_id()
|
|
77
554
|
test_token_usage_accumulates_per_session()
|
|
78
555
|
test_session_stop_removes_session()
|
|
79
556
|
test_snapshot_serializes_cleanly()
|
|
557
|
+
test_graphify_hook_event_increments_scan_count()
|
|
558
|
+
test_graphify_tail_stats_only_does_not_increment_scan_count()
|
|
559
|
+
test_graphify_query_hook_event_increments_query_count()
|
|
80
560
|
test_unknown_event_type_is_ignored_gracefully()
|
|
561
|
+
test_last_context_tokens_set_not_accumulated()
|
|
562
|
+
test_last_context_tokens_in_snapshot()
|
|
563
|
+
test_last_context_tokens_zero_on_fresh_session()
|
|
564
|
+
test_workflow_start_creates_run_in_session()
|
|
565
|
+
test_workflow_stale_sweep_in_snapshot()
|
|
566
|
+
test_workflow_fresh_run_not_staled()
|
|
567
|
+
test_workflow_stale_field_not_in_snapshot()
|
|
568
|
+
test_cold_start_has_no_workflows()
|
|
569
|
+
test_session_stop_ignored_when_agent_active()
|
|
570
|
+
test_session_stop_ignored_when_recent_bus_event()
|
|
571
|
+
test_session_stop_drops_genuinely_idle_session()
|
|
572
|
+
test_live_event_after_idle_stop_resurrects_session()
|
|
573
|
+
test_agent_spawn_alone_confirms_session_and_populates_agents_active()
|
|
574
|
+
test_token_usage_alone_confirms_session_and_applies_tokens()
|
|
575
|
+
test_tombstoned_session_untombstoned_on_live_agent_spawn()
|
|
576
|
+
test_tombstoned_session_subsequent_agent_complete_works_normally()
|
|
577
|
+
test_tombstoned_session_with_no_further_events_stays_gone()
|
|
578
|
+
test_session_start_after_live_confirm_is_harmless_reconfirm()
|
|
81
579
|
print("all state tests passed")
|
|
@@ -87,6 +87,10 @@ async def m():
|
|
|
87
87
|
print(">> memified", flush=True)
|
|
88
88
|
asyncio.run(m())
|
|
89
89
|
PY
|
|
90
|
+
# mktemp creates the runner 0600; docker cp preserves mode + host uid, so the
|
|
91
|
+
# container's non-root user can't read it (Errno 13 Permission denied). Make it
|
|
92
|
+
# world-readable before staging — this was silently blocking ingest fleet-wide.
|
|
93
|
+
chmod 0644 "$PY_SCRIPT"
|
|
90
94
|
docker cp "$PY_SCRIPT" "${CONTAINER}:/home/cognee/_mishkan_ingest.py"
|
|
91
95
|
rm -f "$PY_SCRIPT"
|
|
92
96
|
docker exec -i -w /app/cognee-mcp "$CONTAINER" uv run python -u /home/cognee/_mishkan_ingest.py "$DATASET"
|