@simbimbo/memory-ocmemog 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +11 -9
- package/docs/release-checklist.md +1 -1
- package/ocmemog/sidecar/app.py +1 -0
- package/ocmemog/sidecar/transcript_watcher.py +172 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.11 — 2026-03-20
|
|
4
|
+
|
|
5
|
+
Watcher reliability and release-quality follow-up.
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- prevented duplicate transcript/session turn ingestion in the watcher path
|
|
9
|
+
- propagated `OCMEMOG_API_TOKEN` auth headers on watcher HTTP posts
|
|
10
|
+
- restored persisted queue stats on sidecar startup
|
|
11
|
+
- added durable watcher error logging instead of silent failure swallowing
|
|
12
|
+
- preserved multi-part text content from session message arrays
|
|
13
|
+
- fixed transcript target handling for both directory mode and file mode
|
|
14
|
+
- hardened retry behavior so failed delivery does not silently drop buffered content and session retries preserve transcript provenance without duplicate transcript rows
|
|
15
|
+
- declared `pytest` as a test extra and refreshed release-facing docs/checklists for current validation flow
|
|
16
|
+
|
|
3
17
|
## 0.1.10 — 2026-03-19
|
|
4
18
|
|
|
5
19
|
Release alignment follow-up.
|
package/README.md
CHANGED
|
@@ -162,21 +162,23 @@ launchctl bootstrap gui/$UID scripts/launchagents/com.openclaw.ocmemog.guard.pli
|
|
|
162
162
|
|
|
163
163
|
## Recent changes
|
|
164
164
|
|
|
165
|
-
### 0.1.
|
|
165
|
+
### 0.1.11 (current main)
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
167
|
+
Current main includes the recent memory-quality, governance, and watcher hardening work, including:
|
|
168
|
+
- transcript watcher duplicate-ingest prevention
|
|
169
|
+
- watcher auth propagation for `OCMEMOG_API_TOKEN`
|
|
170
|
+
- persisted queue stats restored on startup
|
|
171
|
+
- durable watcher error logging
|
|
172
|
+
- multi-part text preservation for session content arrays
|
|
173
|
+
- retry-safe session/transcript handling with transcript provenance preserved
|
|
174
|
+
- pytest declared as a repo test extra and available in the local project venv
|
|
173
175
|
|
|
174
176
|
## Release prep / publish
|
|
175
177
|
|
|
176
|
-
|
|
178
|
+
Example ClawHub publish command (update version + changelog first; do not reuse stale release text blindly):
|
|
177
179
|
|
|
178
180
|
```bash
|
|
179
|
-
clawhub publish . --slug memory-ocmemog --name "ocmemog" --version
|
|
181
|
+
clawhub publish . --slug memory-ocmemog --name "ocmemog" --version <next-version> --changelog "<concise release summary>"
|
|
180
182
|
```
|
|
181
183
|
|
|
182
184
|
## Install from npm (after publish)
|
|
@@ -13,7 +13,7 @@ Use this checklist before publishing an ocmemog release.
|
|
|
13
13
|
- [ ] `bash -n scripts/ocmemog-install.sh`
|
|
14
14
|
- [ ] `./scripts/install-ocmemog.sh --help`
|
|
15
15
|
- [ ] `./scripts/install-ocmemog.sh --dry-run`
|
|
16
|
-
- [ ]
|
|
16
|
+
- [ ] `./.venv/bin/python -m pytest -q tests/test_regressions.py tests/test_governance_queue.py tests/test_promotion_governance_integration.py tests/test_hybrid_retrieval.py`
|
|
17
17
|
- [ ] `npm pack --dry-run`
|
|
18
18
|
|
|
19
19
|
## Install flow
|
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -102,6 +102,7 @@ async def _auth_middleware(request: Request, call_next):
|
|
|
102
102
|
|
|
103
103
|
@app.on_event("startup")
|
|
104
104
|
def _start_transcript_watcher() -> None:
|
|
105
|
+
_load_queue_stats()
|
|
105
106
|
enabled = os.environ.get("OCMEMOG_TRANSCRIPT_WATCHER", "").lower() in {"1", "true", "yes"}
|
|
106
107
|
if not enabled:
|
|
107
108
|
return
|
|
@@ -3,10 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
|
+
from collections import deque
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Optional
|
|
8
9
|
from urllib import request as urlrequest
|
|
9
10
|
|
|
11
|
+
from brain.runtime import state_store
|
|
12
|
+
|
|
10
13
|
DEFAULT_ENDPOINT = "http://127.0.0.1:17891/memory/ingest_async"
|
|
11
14
|
DEFAULT_GLOB = "*.log"
|
|
12
15
|
DEFAULT_SESSION_GLOB = "*.jsonl"
|
|
@@ -30,6 +33,23 @@ DEFAULT_REINFORCE_NEGATIVE = [
|
|
|
30
33
|
"disappointed",
|
|
31
34
|
"frustrated",
|
|
32
35
|
]
|
|
36
|
+
WATCHER_ERROR_LOG = state_store.reports_dir() / "ocmemog_transcript_watcher_errors.jsonl"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _log_watcher_error(kind: str, endpoint: str, payload: dict, exc: Exception) -> None:
|
|
40
|
+
try:
|
|
41
|
+
WATCHER_ERROR_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with WATCHER_ERROR_LOG.open("a", encoding="utf-8") as handle:
|
|
43
|
+
handle.write(json.dumps({
|
|
44
|
+
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
45
|
+
"kind": kind,
|
|
46
|
+
"endpoint": endpoint,
|
|
47
|
+
"error_type": type(exc).__name__,
|
|
48
|
+
"error": str(exc),
|
|
49
|
+
"payload_preview": str(payload)[:500],
|
|
50
|
+
}, ensure_ascii=False) + "\n")
|
|
51
|
+
except Exception:
|
|
52
|
+
return
|
|
33
53
|
|
|
34
54
|
|
|
35
55
|
def _pick_latest(path: Path, pattern: str) -> Optional[Path]:
|
|
@@ -41,30 +61,42 @@ def _pick_latest(path: Path, pattern: str) -> Optional[Path]:
|
|
|
41
61
|
return files[-1] if files else None
|
|
42
62
|
|
|
43
63
|
|
|
44
|
-
def
|
|
64
|
+
def _apply_auth_headers(req: urlrequest.Request) -> None:
|
|
65
|
+
token = os.environ.get("OCMEMOG_API_TOKEN", "").strip()
|
|
66
|
+
if token:
|
|
67
|
+
req.add_header("x-ocmemog-token", token)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _post_ingest(endpoint: str, payload: dict) -> bool:
|
|
45
71
|
data = json.dumps(payload).encode("utf-8")
|
|
46
72
|
req = urlrequest.Request(endpoint, data=data, method="POST")
|
|
47
73
|
req.add_header("Content-Type", "application/json")
|
|
74
|
+
_apply_auth_headers(req)
|
|
48
75
|
try:
|
|
49
76
|
with urlrequest.urlopen(req, timeout=10) as resp:
|
|
50
77
|
resp.read()
|
|
51
|
-
|
|
52
|
-
|
|
78
|
+
return True
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
_log_watcher_error("ingest", endpoint, payload, exc)
|
|
81
|
+
return False
|
|
53
82
|
|
|
54
83
|
|
|
55
|
-
def _post_json(endpoint: str, payload: dict) ->
|
|
84
|
+
def _post_json(endpoint: str, payload: dict) -> bool:
|
|
56
85
|
data = json.dumps(payload).encode("utf-8")
|
|
57
86
|
req = urlrequest.Request(endpoint, data=data, method="POST")
|
|
58
87
|
req.add_header("Content-Type", "application/json")
|
|
88
|
+
_apply_auth_headers(req)
|
|
59
89
|
try:
|
|
60
90
|
with urlrequest.urlopen(req, timeout=10) as resp:
|
|
61
91
|
resp.read()
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
return True
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
_log_watcher_error("json", endpoint, payload, exc)
|
|
95
|
+
return False
|
|
64
96
|
|
|
65
97
|
|
|
66
|
-
def _post_turn(endpoint: str, payload: dict) ->
|
|
67
|
-
_post_json(endpoint, payload)
|
|
98
|
+
def _post_turn(endpoint: str, payload: dict) -> bool:
|
|
99
|
+
return _post_json(endpoint, payload)
|
|
68
100
|
|
|
69
101
|
|
|
70
102
|
def _extract_user_text(text: str) -> str:
|
|
@@ -101,6 +133,21 @@ def _extract_conversation_info(text: str) -> dict:
|
|
|
101
133
|
return payload if isinstance(payload, dict) else {}
|
|
102
134
|
|
|
103
135
|
|
|
136
|
+
def _extract_message_text(content: object) -> str:
|
|
137
|
+
if isinstance(content, list):
|
|
138
|
+
parts: list[str] = []
|
|
139
|
+
for item in content:
|
|
140
|
+
if not isinstance(item, dict):
|
|
141
|
+
continue
|
|
142
|
+
if item.get("type") != "text":
|
|
143
|
+
continue
|
|
144
|
+
text = str(item.get("text") or "").strip()
|
|
145
|
+
if text:
|
|
146
|
+
parts.append(text)
|
|
147
|
+
return "\n".join(parts)
|
|
148
|
+
return str(content or "")
|
|
149
|
+
|
|
150
|
+
|
|
104
151
|
def _parse_transcript_line(text: str) -> tuple[Optional[str], str]:
|
|
105
152
|
stripped = text.strip()
|
|
106
153
|
if not stripped:
|
|
@@ -122,10 +169,14 @@ def _count_lines(path: Path) -> int:
|
|
|
122
169
|
|
|
123
170
|
|
|
124
171
|
|
|
125
|
-
def _append_transcript(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
172
|
+
def _append_transcript(transcript_target: Path, timestamp: str, role: str, text: str) -> tuple[Path, int]:
|
|
173
|
+
if transcript_target.suffix:
|
|
174
|
+
path = transcript_target
|
|
175
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
else:
|
|
177
|
+
date = timestamp.split("T")[0] if "T" in timestamp else time.strftime("%Y-%m-%d")
|
|
178
|
+
path = transcript_target / f"{date}.log"
|
|
179
|
+
transcript_target.mkdir(parents=True, exist_ok=True)
|
|
129
180
|
line_no = _count_lines(path) + 1
|
|
130
181
|
with path.open("a", encoding="utf-8") as handle:
|
|
131
182
|
handle.write(f"{timestamp} [{role}] {text}\n")
|
|
@@ -185,6 +236,8 @@ def watch_forever() -> None:
|
|
|
185
236
|
transcript_end_line: Optional[int] = None
|
|
186
237
|
session_start_line: Optional[int] = None
|
|
187
238
|
session_end_line: Optional[int] = None
|
|
239
|
+
recent_session_transcript_lines: deque[tuple[str, int]] = deque(maxlen=max(batch_max * 8, 128))
|
|
240
|
+
pending_session_turns: dict[tuple[str, int], dict[str, object]] = {}
|
|
188
241
|
last_transcript_flush = time.time()
|
|
189
242
|
last_session_flush = time.time()
|
|
190
243
|
|
|
@@ -196,9 +249,9 @@ def watch_forever() -> None:
|
|
|
196
249
|
timestamp: Optional[str],
|
|
197
250
|
start_line: Optional[int],
|
|
198
251
|
end_line: Optional[int],
|
|
199
|
-
) ->
|
|
252
|
+
) -> bool:
|
|
200
253
|
if not buffer:
|
|
201
|
-
return
|
|
254
|
+
return True
|
|
202
255
|
payload = {
|
|
203
256
|
"content": "\n".join(buffer),
|
|
204
257
|
"kind": kind,
|
|
@@ -213,8 +266,10 @@ def watch_forever() -> None:
|
|
|
213
266
|
payload["transcript_end_offset"] = end_line
|
|
214
267
|
if timestamp:
|
|
215
268
|
payload["timestamp"] = timestamp.replace("T", " ")[:19]
|
|
216
|
-
_post_ingest(endpoint, payload)
|
|
217
|
-
|
|
269
|
+
ok = _post_ingest(endpoint, payload)
|
|
270
|
+
if ok:
|
|
271
|
+
buffer.clear()
|
|
272
|
+
return ok
|
|
218
273
|
|
|
219
274
|
def _maybe_reinforce(text: str, timestamp: str) -> None:
|
|
220
275
|
if not reinforce_enabled:
|
|
@@ -266,36 +321,64 @@ def watch_forever() -> None:
|
|
|
266
321
|
try:
|
|
267
322
|
with current_file.open("r", encoding="utf-8", errors="ignore") as handle:
|
|
268
323
|
handle.seek(position)
|
|
269
|
-
|
|
324
|
+
committed_position = position
|
|
325
|
+
committed_line_number = current_line_number
|
|
326
|
+
while True:
|
|
327
|
+
line_start = handle.tell()
|
|
328
|
+
line = handle.readline()
|
|
329
|
+
if not line:
|
|
330
|
+
position = committed_position
|
|
331
|
+
current_line_number = committed_line_number
|
|
332
|
+
break
|
|
270
333
|
text = line.rstrip("\n")
|
|
271
|
-
|
|
334
|
+
next_line_number = committed_line_number + 1
|
|
272
335
|
if not text.strip():
|
|
336
|
+
committed_position = handle.tell()
|
|
337
|
+
committed_line_number = next_line_number
|
|
338
|
+
position = committed_position
|
|
339
|
+
current_line_number = committed_line_number
|
|
340
|
+
continue
|
|
341
|
+
current_marker = (str(current_file), next_line_number)
|
|
342
|
+
if current_marker in recent_session_transcript_lines:
|
|
343
|
+
committed_position = handle.tell()
|
|
344
|
+
committed_line_number = next_line_number
|
|
345
|
+
position = committed_position
|
|
346
|
+
current_line_number = committed_line_number
|
|
273
347
|
continue
|
|
274
348
|
transcript_buffer.append(text)
|
|
275
349
|
transcript_last_path = current_file
|
|
276
350
|
if transcript_start_line is None:
|
|
277
|
-
transcript_start_line =
|
|
278
|
-
transcript_end_line =
|
|
351
|
+
transcript_start_line = next_line_number
|
|
352
|
+
transcript_end_line = next_line_number
|
|
279
353
|
timestamp_value = None
|
|
280
354
|
if text and " " in text:
|
|
281
355
|
timestamp_value = text.split(" ", 1)[0]
|
|
282
356
|
transcript_last_timestamp = timestamp_value
|
|
283
357
|
role, turn_text = _parse_transcript_line(text)
|
|
284
358
|
if role and turn_text:
|
|
285
|
-
_post_turn(
|
|
359
|
+
ok = _post_turn(
|
|
286
360
|
turn_endpoint,
|
|
287
361
|
{
|
|
288
362
|
"role": role,
|
|
289
363
|
"content": turn_text,
|
|
290
364
|
"source": source,
|
|
291
365
|
"transcript_path": str(current_file),
|
|
292
|
-
"transcript_offset":
|
|
293
|
-
"transcript_end_offset":
|
|
366
|
+
"transcript_offset": next_line_number,
|
|
367
|
+
"transcript_end_offset": next_line_number,
|
|
294
368
|
"timestamp": timestamp_value.replace("T", " ")[:19] if timestamp_value else None,
|
|
295
369
|
},
|
|
296
370
|
)
|
|
371
|
+
if not ok:
|
|
372
|
+
if transcript_buffer:
|
|
373
|
+
transcript_buffer.pop()
|
|
374
|
+
if transcript_start_line == next_line_number:
|
|
375
|
+
transcript_start_line = None
|
|
376
|
+
transcript_end_line = committed_line_number if transcript_start_line is not None else None
|
|
377
|
+
position = line_start
|
|
378
|
+
current_line_number = committed_line_number
|
|
379
|
+
break
|
|
297
380
|
if len(transcript_buffer) >= batch_max:
|
|
298
|
-
_flush_buffer(
|
|
381
|
+
ok = _flush_buffer(
|
|
299
382
|
transcript_buffer,
|
|
300
383
|
source_label=source,
|
|
301
384
|
transcript_path=transcript_last_path,
|
|
@@ -303,10 +386,17 @@ def watch_forever() -> None:
|
|
|
303
386
|
start_line=transcript_start_line,
|
|
304
387
|
end_line=transcript_end_line,
|
|
305
388
|
)
|
|
389
|
+
if not ok:
|
|
390
|
+
position = line_start
|
|
391
|
+
current_line_number = committed_line_number
|
|
392
|
+
break
|
|
306
393
|
transcript_start_line = None
|
|
307
394
|
transcript_end_line = None
|
|
308
395
|
last_transcript_flush = time.time()
|
|
309
|
-
|
|
396
|
+
committed_position = handle.tell()
|
|
397
|
+
committed_line_number = next_line_number
|
|
398
|
+
position = committed_position
|
|
399
|
+
current_line_number = committed_line_number
|
|
310
400
|
except Exception:
|
|
311
401
|
pass
|
|
312
402
|
|
|
@@ -324,40 +414,52 @@ def watch_forever() -> None:
|
|
|
324
414
|
try:
|
|
325
415
|
with session_file.open("r", encoding="utf-8", errors="ignore") as handle:
|
|
326
416
|
handle.seek(session_pos)
|
|
327
|
-
|
|
417
|
+
committed_session_pos = session_pos
|
|
418
|
+
while True:
|
|
419
|
+
line_start = handle.tell()
|
|
420
|
+
line = handle.readline()
|
|
421
|
+
if not line:
|
|
422
|
+
session_pos = committed_session_pos
|
|
423
|
+
break
|
|
328
424
|
try:
|
|
329
425
|
entry = json.loads(line)
|
|
330
426
|
except Exception:
|
|
427
|
+
committed_session_pos = handle.tell()
|
|
428
|
+
session_pos = committed_session_pos
|
|
331
429
|
continue
|
|
332
430
|
if entry.get("type") != "message":
|
|
431
|
+
committed_session_pos = handle.tell()
|
|
432
|
+
session_pos = committed_session_pos
|
|
333
433
|
continue
|
|
334
434
|
msg = entry.get("message") or {}
|
|
335
435
|
role = msg.get("role")
|
|
336
436
|
if role not in {"user", "assistant"}:
|
|
437
|
+
committed_session_pos = handle.tell()
|
|
438
|
+
session_pos = committed_session_pos
|
|
337
439
|
continue
|
|
338
440
|
content = msg.get("content")
|
|
339
|
-
|
|
340
|
-
text = next((c.get("text") for c in content if c.get("type") == "text"), "")
|
|
341
|
-
else:
|
|
342
|
-
text = content or ""
|
|
343
|
-
text = str(text).strip()
|
|
441
|
+
text = _extract_message_text(content).strip()
|
|
344
442
|
conversation_info = _extract_conversation_info(text)
|
|
345
443
|
if role == "user":
|
|
346
444
|
text = _extract_user_text(text)
|
|
347
445
|
text = text.replace("\n", " ").strip()
|
|
348
446
|
if not text:
|
|
447
|
+
committed_session_pos = handle.tell()
|
|
448
|
+
session_pos = committed_session_pos
|
|
349
449
|
continue
|
|
350
450
|
timestamp = entry.get("timestamp") or time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
351
451
|
if role == "user":
|
|
352
452
|
_maybe_reinforce(text, timestamp)
|
|
353
|
-
transcript_path, transcript_line_no = _append_transcript(transcript_target, timestamp, role, text)
|
|
354
453
|
session_id = session_file.stem if session_file is not None else None
|
|
355
454
|
message_id = entry.get("id") or conversation_info.get("message_id")
|
|
356
455
|
conversation_id = conversation_info.get("conversation_id") or session_id
|
|
357
456
|
thread_id = conversation_info.get("thread_id") or session_id
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
457
|
+
transcript_line = f"{timestamp} [{role}] {text}"
|
|
458
|
+
retry_key = (str(session_file), line_start)
|
|
459
|
+
pending = pending_session_turns.get(retry_key)
|
|
460
|
+
if pending is None:
|
|
461
|
+
transcript_path, transcript_line_no = _append_transcript(transcript_target, timestamp, role, text)
|
|
462
|
+
turn_payload = {
|
|
361
463
|
"role": role,
|
|
362
464
|
"content": text,
|
|
363
465
|
"conversation_id": conversation_id,
|
|
@@ -365,23 +467,38 @@ def watch_forever() -> None:
|
|
|
365
467
|
"thread_id": thread_id,
|
|
366
468
|
"message_id": message_id,
|
|
367
469
|
"source": "session",
|
|
470
|
+
"timestamp": timestamp.replace("T", " ")[:19],
|
|
368
471
|
"transcript_path": str(transcript_path),
|
|
369
472
|
"transcript_offset": transcript_line_no,
|
|
370
473
|
"transcript_end_offset": transcript_line_no,
|
|
371
|
-
"timestamp": timestamp.replace("T", " ")[:19],
|
|
372
474
|
"metadata": {
|
|
373
475
|
"parent_message_id": entry.get("parentId"),
|
|
374
476
|
},
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
477
|
+
}
|
|
478
|
+
pending_session_turns[retry_key] = {
|
|
479
|
+
"payload": dict(turn_payload),
|
|
480
|
+
"transcript_line": transcript_line,
|
|
481
|
+
"transcript_path": transcript_path,
|
|
482
|
+
"transcript_line_no": transcript_line_no,
|
|
483
|
+
}
|
|
484
|
+
else:
|
|
485
|
+
turn_payload = dict(pending["payload"])
|
|
486
|
+
transcript_line = str(pending["transcript_line"])
|
|
487
|
+
transcript_path = Path(str(pending["transcript_path"]))
|
|
488
|
+
transcript_line_no = int(pending["transcript_line_no"])
|
|
489
|
+
if not _post_turn(turn_endpoint, turn_payload):
|
|
490
|
+
session_pos = line_start
|
|
491
|
+
break
|
|
492
|
+
pending_session_turns.pop(retry_key, None)
|
|
493
|
+
recent_session_transcript_lines.append((str(transcript_path), transcript_line_no))
|
|
494
|
+
session_buffer.append(transcript_line)
|
|
378
495
|
session_last_path = transcript_path
|
|
379
496
|
session_last_timestamp = timestamp
|
|
380
497
|
if session_start_line is None:
|
|
381
498
|
session_start_line = transcript_line_no
|
|
382
499
|
session_end_line = transcript_line_no
|
|
383
500
|
if len(session_buffer) >= batch_max:
|
|
384
|
-
_flush_buffer(
|
|
501
|
+
ok = _flush_buffer(
|
|
385
502
|
session_buffer,
|
|
386
503
|
source_label="session",
|
|
387
504
|
transcript_path=session_last_path,
|
|
@@ -389,16 +506,20 @@ def watch_forever() -> None:
|
|
|
389
506
|
start_line=session_start_line,
|
|
390
507
|
end_line=session_end_line,
|
|
391
508
|
)
|
|
509
|
+
if not ok:
|
|
510
|
+
session_pos = line_start
|
|
511
|
+
break
|
|
392
512
|
session_start_line = None
|
|
393
513
|
session_end_line = None
|
|
394
514
|
last_session_flush = time.time()
|
|
395
|
-
|
|
515
|
+
committed_session_pos = handle.tell()
|
|
516
|
+
session_pos = committed_session_pos
|
|
396
517
|
except Exception:
|
|
397
518
|
pass
|
|
398
519
|
|
|
399
520
|
now = time.time()
|
|
400
521
|
if transcript_buffer and (now - last_transcript_flush) >= batch_seconds:
|
|
401
|
-
_flush_buffer(
|
|
522
|
+
ok = _flush_buffer(
|
|
402
523
|
transcript_buffer,
|
|
403
524
|
source_label=source,
|
|
404
525
|
transcript_path=transcript_last_path,
|
|
@@ -406,11 +527,12 @@ def watch_forever() -> None:
|
|
|
406
527
|
start_line=transcript_start_line,
|
|
407
528
|
end_line=transcript_end_line,
|
|
408
529
|
)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
530
|
+
if ok:
|
|
531
|
+
transcript_start_line = None
|
|
532
|
+
transcript_end_line = None
|
|
533
|
+
last_transcript_flush = now
|
|
412
534
|
if session_buffer and (now - last_session_flush) >= batch_seconds:
|
|
413
|
-
_flush_buffer(
|
|
535
|
+
ok = _flush_buffer(
|
|
414
536
|
session_buffer,
|
|
415
537
|
source_label="session",
|
|
416
538
|
transcript_path=session_last_path,
|
|
@@ -418,8 +540,9 @@ def watch_forever() -> None:
|
|
|
418
540
|
start_line=session_start_line,
|
|
419
541
|
end_line=session_end_line,
|
|
420
542
|
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
543
|
+
if ok:
|
|
544
|
+
session_start_line = None
|
|
545
|
+
session_end_line = None
|
|
546
|
+
last_session_flush = now
|
|
424
547
|
|
|
425
548
|
time.sleep(poll_seconds)
|
package/package.json
CHANGED