opencode-claude-memory 1.5.1 โ 1.6.0
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 +20 -1
- package/bin/opencode-memory +435 -12
- package/package.json +1 -1
- package/src/index.ts +165 -16
- package/src/memory.ts +44 -15
- package/src/memoryScan.ts +130 -0
- package/src/paths.ts +6 -3
- package/src/prompt.ts +187 -112
- package/src/recall.ts +81 -44
package/README.md
CHANGED
|
@@ -122,6 +122,15 @@ The shell hook defines an `opencode()` function that delegates to `opencode-memo
|
|
|
122
122
|
|
|
123
123
|
The implementation ports core logic from Claude Code for path hashing, git-root/worktree handling, memory format, and memory prompting behavior, so both tools can operate on the same files safely.
|
|
124
124
|
|
|
125
|
+
Key modules ported from Claude Code's `src/memdir/`:
|
|
126
|
+
|
|
127
|
+
| Module | Source | Purpose |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `memoryScan.ts` | `memoryScan.ts` | Recursive directory scan + frontmatter header parsing |
|
|
130
|
+
| `recall.ts` | `findRelevantMemories.ts` | Memory recall via keyword scoring (heuristic, no LLM side-query) |
|
|
131
|
+
| `prompt.ts` | `memoryTypes.ts` + `memdir.ts` | System prompt sections, type taxonomy, truncation |
|
|
132
|
+
| `memory.ts` | `memdir.ts` | `truncateEntrypoint()` aligned with `truncateEntrypointContent()` |
|
|
133
|
+
|
|
125
134
|
## ๐ฅ Who this is for
|
|
126
135
|
|
|
127
136
|
- You use **both Claude Code and OpenCode**.
|
|
@@ -174,7 +183,7 @@ Yes. Set `OPENCODE_MEMORY_AUTODREAM=0`. You can also tune gates with:
|
|
|
174
183
|
|
|
175
184
|
### Logs
|
|
176
185
|
|
|
177
|
-
Logs are written to
|
|
186
|
+
Logs are written to `$TMPDIR/opencode-memory-logs/`:
|
|
178
187
|
- `extract-*.log`: automatic memory extraction
|
|
179
188
|
- `dream-*.log`: auto-dream consolidation
|
|
180
189
|
|
|
@@ -215,6 +224,16 @@ Supported memory types:
|
|
|
215
224
|
- `memory_search`: search by keyword
|
|
216
225
|
- `memory_read`: read full memory content
|
|
217
226
|
|
|
227
|
+
## ๐งช Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Run tests
|
|
231
|
+
bun test
|
|
232
|
+
|
|
233
|
+
# No build needed โ raw TS consumed by OpenCode
|
|
234
|
+
# Release: push to main triggers semantic-release โ npm publish
|
|
235
|
+
```
|
|
236
|
+
|
|
218
237
|
## ๐ License
|
|
219
238
|
|
|
220
239
|
[MIT](LICENSE) ยฉ [kuitos](https://github.com/kuitos)
|
package/bin/opencode-memory
CHANGED
|
@@ -40,16 +40,52 @@
|
|
|
40
40
|
|
|
41
41
|
set -euo pipefail
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
resolve_script_path() {
|
|
44
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
45
|
+
python3 - "${BASH_SOURCE[0]}" <<'PY'
|
|
46
|
+
import os
|
|
47
|
+
import sys
|
|
48
|
+
|
|
49
|
+
print(os.path.realpath(sys.argv[1]))
|
|
50
|
+
PY
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
printf '%s\n' "${BASH_SOURCE[0]}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
SCRIPT_PATH="$(resolve_script_path)"
|
|
58
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
|
|
44
59
|
PACKAGE_JSON="$SCRIPT_DIR/../package.json"
|
|
45
60
|
|
|
61
|
+
resolve_package_json() {
|
|
62
|
+
if [ -f "$PACKAGE_JSON" ]; then
|
|
63
|
+
printf '%s\n' "$PACKAGE_JSON"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if command -v npm >/dev/null 2>&1; then
|
|
68
|
+
local npm_root
|
|
69
|
+
npm_root=$(npm root -g 2>/dev/null || true)
|
|
70
|
+
if [ -n "$npm_root" ] && [ -f "$npm_root/opencode-claude-memory/package.json" ]; then
|
|
71
|
+
printf '%s\n' "$npm_root/opencode-claude-memory/package.json"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
printf '%s\n' "$PACKAGE_JSON"
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
print_wrapper_version() {
|
|
47
80
|
local version
|
|
81
|
+
local package_json
|
|
82
|
+
|
|
83
|
+
package_json="$(resolve_package_json)"
|
|
48
84
|
|
|
49
|
-
version=$(awk -F'"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' "$
|
|
85
|
+
version=$(awk -F'"' '/^[[:space:]]*"version"[[:space:]]*:/ { print $4; exit }' "$package_json")
|
|
50
86
|
|
|
51
87
|
if [ -z "$version" ]; then
|
|
52
|
-
echo "[opencode-memory] ERROR: Cannot read package version from $
|
|
88
|
+
echo "[opencode-memory] ERROR: Cannot read package version from $package_json" >&2
|
|
53
89
|
exit 1
|
|
54
90
|
fi
|
|
55
91
|
|
|
@@ -184,6 +220,35 @@ find_real_opencode() {
|
|
|
184
220
|
|
|
185
221
|
REAL_OPENCODE="$(find_real_opencode)"
|
|
186
222
|
|
|
223
|
+
is_opencode_subcommand() {
|
|
224
|
+
case "$1" in
|
|
225
|
+
completion|acp|mcp|attach|run|debug|providers|auth|agent|upgrade|uninstall|serve|web|models|stats|export|import|github|pr|session|plugin|plug|db)
|
|
226
|
+
return 0
|
|
227
|
+
;;
|
|
228
|
+
*)
|
|
229
|
+
return 1
|
|
230
|
+
;;
|
|
231
|
+
esac
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
extract_working_dir_from_args() {
|
|
235
|
+
local previous=""
|
|
236
|
+
for arg in "$@"; do
|
|
237
|
+
if [ "$previous" = "--dir" ]; then
|
|
238
|
+
printf '%s\n' "$arg"
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
previous="$arg"
|
|
242
|
+
done
|
|
243
|
+
|
|
244
|
+
if [ "$#" -gt 0 ] && [ -n "${1:-}" ] && [[ "${1:-}" != -* ]] && ! is_opencode_subcommand "$1" && [ -d "$1" ]; then
|
|
245
|
+
printf '%s\n' "$1"
|
|
246
|
+
return 0
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
return 1
|
|
250
|
+
}
|
|
251
|
+
|
|
187
252
|
# ============================================================================
|
|
188
253
|
# Configuration
|
|
189
254
|
# ============================================================================
|
|
@@ -201,10 +266,17 @@ AUTODREAM_SCAN_LIMIT="${OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT:-200}"
|
|
|
201
266
|
AUTODREAM_MODEL="${OPENCODE_MEMORY_AUTODREAM_MODEL:-$EXTRACT_MODEL}"
|
|
202
267
|
AUTODREAM_AGENT="${OPENCODE_MEMORY_AUTODREAM_AGENT:-$EXTRACT_AGENT}"
|
|
203
268
|
AUTODREAM_STALE_LOCK_SECS=$((60 * 60))
|
|
269
|
+
SESSION_WAIT_SECONDS="${OPENCODE_MEMORY_SESSION_WAIT_SECONDS:-5}"
|
|
204
270
|
|
|
205
|
-
WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(pwd)}"
|
|
271
|
+
WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(extract_working_dir_from_args "$@" || pwd)}"
|
|
206
272
|
|
|
207
|
-
|
|
273
|
+
TMP_BASE_DIR="${TMPDIR:-/tmp}"
|
|
274
|
+
while [ "$TMP_BASE_DIR" != "/" ] && [ "${TMP_BASE_DIR%/}" != "$TMP_BASE_DIR" ]; do
|
|
275
|
+
TMP_BASE_DIR="${TMP_BASE_DIR%/}"
|
|
276
|
+
done
|
|
277
|
+
if [ -z "$TMP_BASE_DIR" ]; then
|
|
278
|
+
TMP_BASE_DIR="/"
|
|
279
|
+
fi
|
|
208
280
|
|
|
209
281
|
# Scope lock files at project root granularity (not per-subdirectory).
|
|
210
282
|
PROJECT_SCOPE_DIR="$WORKING_DIR"
|
|
@@ -215,7 +287,7 @@ fi
|
|
|
215
287
|
PROJECT_KEY="$(printf '%s' "$PROJECT_SCOPE_DIR" | cksum | awk '{print $1}')"
|
|
216
288
|
|
|
217
289
|
# Lock files (prevent concurrent work on the same project)
|
|
218
|
-
LOCK_DIR="/
|
|
290
|
+
LOCK_DIR="$TMP_BASE_DIR/opencode-memory-locks"
|
|
219
291
|
mkdir -p "$LOCK_DIR"
|
|
220
292
|
EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
|
|
221
293
|
|
|
@@ -224,7 +296,7 @@ mkdir -p "$STATE_DIR"
|
|
|
224
296
|
CONSOLIDATION_LOCK_FILE="$STATE_DIR/${PROJECT_KEY}.consolidate-lock"
|
|
225
297
|
|
|
226
298
|
# Logs
|
|
227
|
-
LOG_DIR="$
|
|
299
|
+
LOG_DIR="$TMP_BASE_DIR/opencode-memory-logs"
|
|
228
300
|
mkdir -p "$LOG_DIR"
|
|
229
301
|
TASK_LOG_PREFIX="$(date +%Y%m%d-%H%M%S)-${PROJECT_KEY}"
|
|
230
302
|
EXTRACT_LOG_FILE="$LOG_DIR/extract-${TASK_LOG_PREFIX}.log"
|
|
@@ -401,6 +473,351 @@ get_latest_session_id() {
|
|
|
401
473
|
fi
|
|
402
474
|
}
|
|
403
475
|
|
|
476
|
+
get_session_target_id() {
|
|
477
|
+
local before_json="$1"
|
|
478
|
+
local started_at_ms="$2"
|
|
479
|
+
local workdir="$3"
|
|
480
|
+
local project_dir="$4"
|
|
481
|
+
local after_json
|
|
482
|
+
|
|
483
|
+
after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
|
|
484
|
+
|
|
485
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
486
|
+
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" <<'PY'
|
|
487
|
+
import json
|
|
488
|
+
import os
|
|
489
|
+
import sys
|
|
490
|
+
|
|
491
|
+
before_raw, after_raw, started_at_ms_raw, workdir, project_dir = sys.argv[1:6]
|
|
492
|
+
|
|
493
|
+
def parse(raw):
|
|
494
|
+
try:
|
|
495
|
+
data = json.loads(raw)
|
|
496
|
+
return data if isinstance(data, list) else []
|
|
497
|
+
except Exception:
|
|
498
|
+
return []
|
|
499
|
+
|
|
500
|
+
def timestamp(item):
|
|
501
|
+
time_obj = item.get("time") if isinstance(item.get("time"), dict) else {}
|
|
502
|
+
for key in ("updated", "created"):
|
|
503
|
+
value = item.get(key)
|
|
504
|
+
if value is not None:
|
|
505
|
+
try:
|
|
506
|
+
return int(value)
|
|
507
|
+
except Exception:
|
|
508
|
+
pass
|
|
509
|
+
value = time_obj.get(key)
|
|
510
|
+
if value is not None:
|
|
511
|
+
try:
|
|
512
|
+
return int(value)
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
return 0
|
|
516
|
+
|
|
517
|
+
def normalize(path):
|
|
518
|
+
if not path:
|
|
519
|
+
return ""
|
|
520
|
+
return os.path.realpath(path)
|
|
521
|
+
|
|
522
|
+
before = parse(before_raw)
|
|
523
|
+
after = parse(after_raw)
|
|
524
|
+
started_at_ms = int(started_at_ms_raw or "0")
|
|
525
|
+
before_ids = {item.get("id") for item in before if item.get("id")}
|
|
526
|
+
workdir = normalize(workdir)
|
|
527
|
+
project_dir = normalize(project_dir)
|
|
528
|
+
|
|
529
|
+
def in_scope(item):
|
|
530
|
+
directory = normalize(item.get("directory", ""))
|
|
531
|
+
return directory != "" and directory in {workdir, project_dir}
|
|
532
|
+
|
|
533
|
+
def choose(candidates):
|
|
534
|
+
ranked = sorted(
|
|
535
|
+
[item for item in candidates if item.get("id")],
|
|
536
|
+
key=lambda item: timestamp(item),
|
|
537
|
+
reverse=True,
|
|
538
|
+
)
|
|
539
|
+
if ranked:
|
|
540
|
+
print(ranked[0]["id"])
|
|
541
|
+
return True
|
|
542
|
+
return False
|
|
543
|
+
|
|
544
|
+
new_sessions = [item for item in after if item.get("id") not in before_ids]
|
|
545
|
+
updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
|
|
546
|
+
|
|
547
|
+
for pool in (
|
|
548
|
+
[item for item in new_sessions if in_scope(item)],
|
|
549
|
+
[item for item in updated_sessions if in_scope(item)],
|
|
550
|
+
[item for item in after if in_scope(item)],
|
|
551
|
+
):
|
|
552
|
+
if choose(pool):
|
|
553
|
+
break
|
|
554
|
+
PY
|
|
555
|
+
return 0
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
get_latest_session_id
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
get_transcripts_dir() {
|
|
562
|
+
printf '%s\n' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/transcripts"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
get_storage_session_diff_dir() {
|
|
566
|
+
printf '%s\n' "$HOME/.local/share/opencode/storage/session_diff"
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
resolve_session_directory() {
|
|
570
|
+
local session_id="$1"
|
|
571
|
+
local output_file
|
|
572
|
+
|
|
573
|
+
output_file=$(mktemp)
|
|
574
|
+
if ! "$REAL_OPENCODE" export "$session_id" >"$output_file" 2>/dev/null; then
|
|
575
|
+
rm -f "$output_file"
|
|
576
|
+
return 1
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
if [ ! -s "$output_file" ]; then
|
|
580
|
+
rm -f "$output_file"
|
|
581
|
+
return 1
|
|
582
|
+
fi
|
|
583
|
+
|
|
584
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
585
|
+
python3 - "$output_file" <<'PY'
|
|
586
|
+
import json
|
|
587
|
+
from pathlib import Path
|
|
588
|
+
import sys
|
|
589
|
+
|
|
590
|
+
raw = Path(sys.argv[1]).read_text()
|
|
591
|
+
start = raw.find('{')
|
|
592
|
+
if start == -1:
|
|
593
|
+
raise SystemExit(1)
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
data = json.loads(raw[start:])
|
|
597
|
+
except Exception:
|
|
598
|
+
raise SystemExit(1)
|
|
599
|
+
|
|
600
|
+
directory = ((data.get("info") or {}).get("directory") or "")
|
|
601
|
+
if directory:
|
|
602
|
+
print(directory)
|
|
603
|
+
PY
|
|
604
|
+
local status=$?
|
|
605
|
+
rm -f "$output_file"
|
|
606
|
+
return $status
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
rm -f "$output_file"
|
|
610
|
+
return 1
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
get_scoped_artifact_session_id_since() {
|
|
614
|
+
local timestamp_file="$1"
|
|
615
|
+
local workdir="$2"
|
|
616
|
+
local project_dir="$3"
|
|
617
|
+
|
|
618
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
619
|
+
return 1
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
local candidates
|
|
623
|
+
candidates=$(python3 - "$timestamp_file" "$HOME" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" <<'PY'
|
|
624
|
+
import os
|
|
625
|
+
import sys
|
|
626
|
+
|
|
627
|
+
timestamp_file, home_dir, claude_dir = sys.argv[1:4]
|
|
628
|
+
|
|
629
|
+
try:
|
|
630
|
+
threshold = os.path.getmtime(timestamp_file)
|
|
631
|
+
except OSError:
|
|
632
|
+
raise SystemExit(1)
|
|
633
|
+
|
|
634
|
+
sources = [
|
|
635
|
+
(os.path.join(home_dir, '.local', 'share', 'opencode', 'storage', 'session_diff'), '.json'),
|
|
636
|
+
(os.path.join(claude_dir, 'transcripts'), '.jsonl'),
|
|
637
|
+
]
|
|
638
|
+
|
|
639
|
+
latest = {}
|
|
640
|
+
for directory, suffix in sources:
|
|
641
|
+
if not os.path.isdir(directory):
|
|
642
|
+
continue
|
|
643
|
+
for entry in os.scandir(directory):
|
|
644
|
+
if not entry.is_file() or not entry.name.endswith(suffix):
|
|
645
|
+
continue
|
|
646
|
+
try:
|
|
647
|
+
mtime = entry.stat().st_mtime
|
|
648
|
+
except OSError:
|
|
649
|
+
continue
|
|
650
|
+
if mtime <= threshold:
|
|
651
|
+
continue
|
|
652
|
+
session_id = entry.name[:-len(suffix)]
|
|
653
|
+
latest[session_id] = max(latest.get(session_id, -1), mtime)
|
|
654
|
+
|
|
655
|
+
for session_id, mtime in sorted(latest.items(), key=lambda item: item[1], reverse=True):
|
|
656
|
+
print(session_id)
|
|
657
|
+
PY
|
|
658
|
+
) || return 1
|
|
659
|
+
|
|
660
|
+
local session_id=""
|
|
661
|
+
local session_dir=""
|
|
662
|
+
local normalized_session_dir=""
|
|
663
|
+
local normalized_workdir=""
|
|
664
|
+
local normalized_project_dir=""
|
|
665
|
+
normalized_workdir=$(python3 - "$workdir" <<'PY'
|
|
666
|
+
import os, sys
|
|
667
|
+
print(os.path.realpath(sys.argv[1]))
|
|
668
|
+
PY
|
|
669
|
+
)
|
|
670
|
+
normalized_project_dir=$(python3 - "$project_dir" <<'PY'
|
|
671
|
+
import os, sys
|
|
672
|
+
print(os.path.realpath(sys.argv[1]))
|
|
673
|
+
PY
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
while IFS= read -r session_id; do
|
|
677
|
+
[ -n "$session_id" ] || continue
|
|
678
|
+
session_dir=$(resolve_session_directory "$session_id" || true)
|
|
679
|
+
[ -n "$session_dir" ] || continue
|
|
680
|
+
normalized_session_dir=$(python3 - "$session_dir" <<'PY'
|
|
681
|
+
import os, sys
|
|
682
|
+
print(os.path.realpath(sys.argv[1]))
|
|
683
|
+
PY
|
|
684
|
+
)
|
|
685
|
+
if [ "$normalized_session_dir" = "$normalized_workdir" ] || [ "$normalized_session_dir" = "$normalized_project_dir" ]; then
|
|
686
|
+
printf '%s\n' "$session_id"
|
|
687
|
+
return 0
|
|
688
|
+
fi
|
|
689
|
+
done <<EOF
|
|
690
|
+
$candidates
|
|
691
|
+
EOF
|
|
692
|
+
|
|
693
|
+
return 1
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
get_latest_storage_session_id_since() {
|
|
697
|
+
local timestamp_file="$1"
|
|
698
|
+
local storage_dir
|
|
699
|
+
storage_dir=$(get_storage_session_diff_dir)
|
|
700
|
+
|
|
701
|
+
if [ ! -d "$storage_dir" ]; then
|
|
702
|
+
return 1
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
706
|
+
python3 - "$storage_dir" "$timestamp_file" <<'PY'
|
|
707
|
+
import os
|
|
708
|
+
import sys
|
|
709
|
+
|
|
710
|
+
storage_dir, timestamp_file = sys.argv[1:3]
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
threshold = os.path.getmtime(timestamp_file)
|
|
714
|
+
except OSError:
|
|
715
|
+
raise SystemExit(1)
|
|
716
|
+
|
|
717
|
+
latest = None
|
|
718
|
+
latest_mtime = -1.0
|
|
719
|
+
|
|
720
|
+
for entry in os.scandir(storage_dir):
|
|
721
|
+
if not entry.is_file() or not entry.name.endswith('.json'):
|
|
722
|
+
continue
|
|
723
|
+
try:
|
|
724
|
+
mtime = entry.stat().st_mtime
|
|
725
|
+
except OSError:
|
|
726
|
+
continue
|
|
727
|
+
if mtime <= threshold:
|
|
728
|
+
continue
|
|
729
|
+
if mtime > latest_mtime:
|
|
730
|
+
latest_mtime = mtime
|
|
731
|
+
latest = entry.name[:-5]
|
|
732
|
+
|
|
733
|
+
if latest:
|
|
734
|
+
print(latest)
|
|
735
|
+
PY
|
|
736
|
+
return 0
|
|
737
|
+
fi
|
|
738
|
+
|
|
739
|
+
return 1
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
get_latest_transcript_session_id_since() {
|
|
743
|
+
local timestamp_file="$1"
|
|
744
|
+
local transcripts_dir
|
|
745
|
+
transcripts_dir=$(get_transcripts_dir)
|
|
746
|
+
|
|
747
|
+
if [ ! -d "$transcripts_dir" ]; then
|
|
748
|
+
return 1
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
752
|
+
python3 - "$transcripts_dir" "$timestamp_file" <<'PY'
|
|
753
|
+
import os
|
|
754
|
+
import sys
|
|
755
|
+
|
|
756
|
+
transcripts_dir, timestamp_file = sys.argv[1:3]
|
|
757
|
+
|
|
758
|
+
try:
|
|
759
|
+
threshold = os.path.getmtime(timestamp_file)
|
|
760
|
+
except OSError:
|
|
761
|
+
raise SystemExit(1)
|
|
762
|
+
|
|
763
|
+
latest = None
|
|
764
|
+
latest_mtime = -1.0
|
|
765
|
+
|
|
766
|
+
for entry in os.scandir(transcripts_dir):
|
|
767
|
+
if not entry.is_file() or not entry.name.endswith('.jsonl'):
|
|
768
|
+
continue
|
|
769
|
+
try:
|
|
770
|
+
mtime = entry.stat().st_mtime
|
|
771
|
+
except OSError:
|
|
772
|
+
continue
|
|
773
|
+
if mtime <= threshold:
|
|
774
|
+
continue
|
|
775
|
+
if mtime > latest_mtime:
|
|
776
|
+
latest_mtime = mtime
|
|
777
|
+
latest = entry.name[:-6]
|
|
778
|
+
|
|
779
|
+
if latest:
|
|
780
|
+
print(latest)
|
|
781
|
+
PY
|
|
782
|
+
return 0
|
|
783
|
+
fi
|
|
784
|
+
|
|
785
|
+
return 1
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
main_prompt_requests_ignore_memory() {
|
|
789
|
+
if [ "${1:-}" != "run" ]; then
|
|
790
|
+
return 1
|
|
791
|
+
fi
|
|
792
|
+
|
|
793
|
+
local joined
|
|
794
|
+
joined=$(printf '%s\n' "$*" | tr '[:upper:]' '[:lower:]')
|
|
795
|
+
printf '%s\n' "$joined" | grep -Eq "(ignore|don't use|do not use|without|skip)[[:space:]]+(the[[:space:]]+)?memory|memory[[:space:]]+((should|must)[[:space:]]+be[[:space:]]+)?ignored"
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
wait_for_session_target_id() {
|
|
799
|
+
local before_json="$1"
|
|
800
|
+
local started_at_ms="$2"
|
|
801
|
+
local wait_seconds="${3:-5}"
|
|
802
|
+
local attempt=0
|
|
803
|
+
local session_id=""
|
|
804
|
+
|
|
805
|
+
while [ "$attempt" -lt "$wait_seconds" ]; do
|
|
806
|
+
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
807
|
+
if [ -z "$session_id" ]; then
|
|
808
|
+
session_id=$(get_scoped_artifact_session_id_since "$TIMESTAMP_FILE" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
809
|
+
fi
|
|
810
|
+
if [ -n "$session_id" ]; then
|
|
811
|
+
printf '%s\n' "$session_id"
|
|
812
|
+
return 0
|
|
813
|
+
fi
|
|
814
|
+
sleep 1
|
|
815
|
+
attempt=$((attempt + 1))
|
|
816
|
+
done
|
|
817
|
+
|
|
818
|
+
return 1
|
|
819
|
+
}
|
|
820
|
+
|
|
404
821
|
file_mtime_secs() {
|
|
405
822
|
local file="$1"
|
|
406
823
|
if [ ! -f "$file" ]; then
|
|
@@ -559,7 +976,7 @@ run_extraction_if_needed() {
|
|
|
559
976
|
log "Extracting memories from session $session_id..."
|
|
560
977
|
log "Extraction log: $EXTRACT_LOG_FILE"
|
|
561
978
|
|
|
562
|
-
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
|
|
979
|
+
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
563
980
|
if [ -n "$EXTRACT_MODEL" ]; then
|
|
564
981
|
cmd+=(-m "$EXTRACT_MODEL")
|
|
565
982
|
fi
|
|
@@ -617,7 +1034,7 @@ run_autodream_if_needed() {
|
|
|
617
1034
|
log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
|
|
618
1035
|
log "Auto-dream log: $AUTODREAM_LOG_FILE"
|
|
619
1036
|
|
|
620
|
-
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
|
|
1037
|
+
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
621
1038
|
if [ -n "$AUTODREAM_MODEL" ]; then
|
|
622
1039
|
cmd+=(-m "$AUTODREAM_MODEL")
|
|
623
1040
|
fi
|
|
@@ -651,10 +1068,16 @@ run_post_session_tasks() {
|
|
|
651
1068
|
|
|
652
1069
|
# Step 0: Create timestamp marker before running opencode
|
|
653
1070
|
TIMESTAMP_FILE=$(mktemp)
|
|
1071
|
+
SESSION_CAPTURE_STARTED_AT_MS=$(( $(date +%s) * 1000 ))
|
|
1072
|
+
PRE_SESSION_JSON=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
|
|
654
1073
|
|
|
655
1074
|
# Step 1: Run the real opencode with all original arguments, capture exit code
|
|
656
1075
|
opencode_exit=0
|
|
657
|
-
|
|
1076
|
+
if main_prompt_requests_ignore_memory "$@"; then
|
|
1077
|
+
OPENCODE_MEMORY_IGNORE=1 "$REAL_OPENCODE" "$@" || opencode_exit=$?
|
|
1078
|
+
else
|
|
1079
|
+
"$REAL_OPENCODE" "$@" || opencode_exit=$?
|
|
1080
|
+
fi
|
|
658
1081
|
|
|
659
1082
|
# Step 2: If no maintenance task is enabled, exit early
|
|
660
1083
|
if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
|
|
@@ -662,8 +1085,8 @@ if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
|
|
|
662
1085
|
exit $opencode_exit
|
|
663
1086
|
fi
|
|
664
1087
|
|
|
665
|
-
# Step 3:
|
|
666
|
-
session_id=$(
|
|
1088
|
+
# Step 3: Capture the session ID for this invocation
|
|
1089
|
+
session_id=$(wait_for_session_target_id "$PRE_SESSION_JSON" "$SESSION_CAPTURE_STARTED_AT_MS" "$SESSION_WAIT_SECONDS" || true)
|
|
667
1090
|
if [ -z "$session_id" ]; then
|
|
668
1091
|
log "No session found, skipping post-session memory maintenance"
|
|
669
1092
|
cleanup_timestamp
|
package/package.json
CHANGED