mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
W4: gstack-brain-sync holds staged transcript pages without consent
Queued transcripts/ pages stay queued and on disk unless transcript_ingest_mode is recent or all; other classes still push. Test: brain-sync.test.ts push-filter case (absent, off, legacy A, then recent).
This commit is contained in:
1 parent
7da92a7741
commit
cece11076e
2 files changed
+55
-3
No files matched your search
+22
-3
@@ -245,10 +245,11 @@ compute_paths_to_stage() {
|
||||
local mode="$1"
|
||||
local class_file="${2:-}"
|
||||
local snapshot_file="${3:-}"
|
||||
python3 - "$GSTACK_HOME" "$QUEUE_DIR" "$ALLOWLIST" "$PRIVACY_MAP" "$SKIP_FILE" "$mode" "$class_file" "$snapshot_file" <<'PYEOF'
|
||||
local transcripts_ok="${4:-0}"
|
||||
python3 - "$GSTACK_HOME" "$QUEUE_DIR" "$ALLOWLIST" "$PRIVACY_MAP" "$SKIP_FILE" "$mode" "$class_file" "$snapshot_file" "$transcripts_ok" <<'PYEOF'
|
||||
import sys, json, os, fnmatch, glob
|
||||
|
||||
gstack_home, spool_dir, allowlist_path, privacy_path, skip_path, mode, class_file, snapshot_file = sys.argv[1:9]
|
||||
gstack_home, spool_dir, allowlist_path, privacy_path, skip_path, mode, class_file, snapshot_file, transcripts_ok = sys.argv[1:10]
|
||||
|
||||
def load_lines(path):
|
||||
try:
|
||||
@@ -365,6 +366,11 @@ for p in sorted(queue_paths):
|
||||
if not path_matches_any(p, allowlist_globs):
|
||||
classified["dropped"]["unmatched"].append(p)
|
||||
continue
|
||||
# Staged transcript pages need transcript consent; without it they stay
|
||||
# queued (retained) and on disk, and push once the user consents.
|
||||
if transcripts_ok != "1" and p.split("/")[0] == "transcripts":
|
||||
classified["retained"].append(p)
|
||||
continue
|
||||
# Must survive privacy mode filter — held entries STAY QUEUED (retained):
|
||||
# they would sync under a higher artifacts_sync_mode, and reporting them
|
||||
# as "no allowlisted changes" was #2549's misattribution.
|
||||
@@ -387,6 +393,17 @@ for p in final:
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# Transcript consent: transcript_ingest_mode is recent or all (trimmed, any
|
||||
# case). An absent key, off, a legacy value or anything else is not consent.
|
||||
# Same rule as normalizeTranscriptConsent in gstack-memory-ingest.ts.
|
||||
transcripts_consented() {
|
||||
"$CONFIG_BIN" has transcript_ingest_mode 2>/dev/null || return 1
|
||||
local v
|
||||
v=$("$CONFIG_BIN" get transcript_ingest_mode 2>/dev/null || true)
|
||||
v=$(printf '%s' "$v" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
||||
[ "$v" = "recent" ] || [ "$v" = "all" ]
|
||||
}
|
||||
|
||||
# Finalize the drain: delete exactly the spool record files this drain
|
||||
# consumed (per the snapshot manifest) AND positively classified. Deletion is
|
||||
# EXPLICIT-DELETE-ONLY: a record is unlinked only when its path appears in
|
||||
@@ -659,7 +676,9 @@ subcmd_once() {
|
||||
# snapshot manifest but no classification. Finalizing on that state is what
|
||||
# used to wipe the queue — so on a nonzero exit, warn loudly, do NOT call
|
||||
# finalize_queue, and leave everything queued for the next drain.
|
||||
if ! compute_paths_to_stage "$mode" "$class_file" "$snapshot_file" > "$paths_file"; then
|
||||
local transcripts_ok=0
|
||||
transcripts_consented && transcripts_ok=1
|
||||
if ! compute_paths_to_stage "$mode" "$class_file" "$snapshot_file" "$transcripts_ok" > "$paths_file"; then
|
||||
echo "BRAIN_SYNC: warning: queue classification failed — no records consumed; next run re-drains" >&2
|
||||
write_status "error" "classification failed; queue preserved (next run retries)"
|
||||
exit 0
|
||||
|
||||
@@ -294,6 +294,39 @@ describe('init + sync + restore round-trip', () => {
|
||||
expect(log.stdout).toMatch(/sync: 1 file/);
|
||||
});
|
||||
|
||||
test('staged transcript pages stay queued without transcript consent; other classes still push', () => {
|
||||
run(['gstack-artifacts-init', '--remote', bareRemote]);
|
||||
run(['gstack-config', 'set', 'artifacts_sync_mode', 'full']);
|
||||
fs.mkdirSync(path.join(tmpHome, 'projects', 'p'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpHome, 'projects/p/learnings.jsonl'),
|
||||
'{"skill":"x","insight":"y","ts":"2026-04-22T10:00:00Z"}\n');
|
||||
const page = 'transcripts/run-1-1/session.md';
|
||||
fs.mkdirSync(path.join(tmpHome, 'transcripts', 'run-1-1'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpHome, page), '# Session\n\nhello\n');
|
||||
run(['gstack-brain-enqueue', 'projects/p/learnings.jsonl']);
|
||||
run(['gstack-brain-enqueue', page]);
|
||||
const remoteFiles = () =>
|
||||
spawnSync('git', ['--git-dir=' + bareRemote, 'ls-tree', '-r', '--name-only', 'main'], { encoding: 'utf-8', timeout: 30_000 }).stdout;
|
||||
|
||||
expect(run(['gstack-brain-sync', '--once']).status).toBe(0);
|
||||
expect(remoteFiles()).toContain('projects/p/learnings.jsonl');
|
||||
expect(remoteFiles()).not.toContain(page);
|
||||
expect(spoolText()).toContain(page);
|
||||
expect(fs.existsSync(path.join(tmpHome, page))).toBe(true);
|
||||
|
||||
// A stored off or legacy value is not consent either.
|
||||
for (const value of ['off', 'A']) {
|
||||
fs.appendFileSync(path.join(tmpHome, 'config.yaml'), `transcript_ingest_mode: ${value}\n`);
|
||||
expect(run(['gstack-brain-sync', '--once']).status).toBe(0);
|
||||
expect(remoteFiles()).not.toContain(page);
|
||||
}
|
||||
|
||||
expect(run(['gstack-config', 'set', 'transcript_ingest_mode', 'recent']).status).toBe(0);
|
||||
expect(run(['gstack-brain-sync', '--once']).status).toBe(0);
|
||||
expect(remoteFiles()).toContain(page);
|
||||
expect(spoolText()).not.toContain(page);
|
||||
});
|
||||
|
||||
test('restore round-trip: writes on machine A visible on machine B', () => {
|
||||
// Machine A.
|
||||
run(['gstack-artifacts-init', '--remote', bareRemote]);
|
||||
|
||||
Reference in new issue
Block a user