@meetless/mla 0.1.4
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/LICENSE +201 -0
- package/README.md +81 -0
- package/dist/build-info.json +9 -0
- package/dist/bundles/ask-core.js +396 -0
- package/dist/bundles/mcp.js +16592 -0
- package/dist/bundles/trace-core.js +263 -0
- package/dist/cli.js +828 -0
- package/dist/commands/activate.js +781 -0
- package/dist/commands/adoption.js +130 -0
- package/dist/commands/ask.js +290 -0
- package/dist/commands/context.js +114 -0
- package/dist/commands/debug.js +313 -0
- package/dist/commands/doctor.js +1021 -0
- package/dist/commands/enrich.js +427 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/flush.js +184 -0
- package/dist/commands/graph.js +104 -0
- package/dist/commands/init.js +272 -0
- package/dist/commands/internal-active-review.js +322 -0
- package/dist/commands/internal-auto-index.js +188 -0
- package/dist/commands/internal-capture-decisions.js +320 -0
- package/dist/commands/internal-evidence-correlate.js +239 -0
- package/dist/commands/internal-evidence-hooks.js +240 -0
- package/dist/commands/internal-evidence-inject.js +231 -0
- package/dist/commands/internal-finalize.js +221 -0
- package/dist/commands/internal-pretool-observe.js +225 -0
- package/dist/commands/internal-refresh.js +136 -0
- package/dist/commands/internal-session-nudge.js +120 -0
- package/dist/commands/internal-steer-sync.js +117 -0
- package/dist/commands/internal-turn-recap.js +140 -0
- package/dist/commands/kb.js +375 -0
- package/dist/commands/kb_add.js +681 -0
- package/dist/commands/kb_forget.js +283 -0
- package/dist/commands/kb_move.js +45 -0
- package/dist/commands/kb_pending.js +410 -0
- package/dist/commands/kb_personal.js +149 -0
- package/dist/commands/kb_promote.js +188 -0
- package/dist/commands/kb_purge.js +168 -0
- package/dist/commands/kb_reingest.js +335 -0
- package/dist/commands/kb_retime.js +170 -0
- package/dist/commands/kb_review.js +391 -0
- package/dist/commands/kb_revision.js +179 -0
- package/dist/commands/kb_show.js +385 -0
- package/dist/commands/label.js +226 -0
- package/dist/commands/login.js +295 -0
- package/dist/commands/logout.js +108 -0
- package/dist/commands/mcp-supervisor.js +93 -0
- package/dist/commands/mcp.js +227 -0
- package/dist/commands/queue-prune.js +98 -0
- package/dist/commands/review.js +358 -0
- package/dist/commands/rewire.js +124 -0
- package/dist/commands/rules.js +728 -0
- package/dist/commands/scan-context.js +67 -0
- package/dist/commands/session.js +347 -0
- package/dist/commands/stats.js +479 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/summary.js +250 -0
- package/dist/commands/turn.js +114 -0
- package/dist/commands/uninstall.js +222 -0
- package/dist/commands/whoami.js +102 -0
- package/dist/commands/workspace.js +130 -0
- package/dist/hooks-template/ce0-post-tool-use.sh +34 -0
- package/dist/hooks-template/ce0-session-start.sh +49 -0
- package/dist/hooks-template/ce0-stop.sh +29 -0
- package/dist/hooks-template/ce0-user-prompt-submit.sh +38 -0
- package/dist/hooks-template/common.sh +934 -0
- package/dist/hooks-template/event-batch-filter.jq +67 -0
- package/dist/hooks-template/flush.sh +503 -0
- package/dist/hooks-template/post-tool-use.sh +423 -0
- package/dist/hooks-template/pre-tool-use.sh +69 -0
- package/dist/hooks-template/session-start.sh +140 -0
- package/dist/hooks-template/stop.sh +308 -0
- package/dist/hooks-template/user-prompt-submit.sh +1162 -0
- package/dist/lib/activation.js +79 -0
- package/dist/lib/active-conflict-cache.js +141 -0
- package/dist/lib/active-memory.js +59 -0
- package/dist/lib/active-review-runner.js +26 -0
- package/dist/lib/agent-decision/index.js +25 -0
- package/dist/lib/agent-decision/keys.js +49 -0
- package/dist/lib/agent-decision/normalize-claude.js +183 -0
- package/dist/lib/agent-decision/types.js +21 -0
- package/dist/lib/agent-decision/validate.js +216 -0
- package/dist/lib/analytics/capture.js +96 -0
- package/dist/lib/analytics/command-event.js +267 -0
- package/dist/lib/analytics/consent.js +58 -0
- package/dist/lib/analytics/coverage-gap.js +96 -0
- package/dist/lib/analytics/envelope.js +236 -0
- package/dist/lib/analytics/event-id.js +86 -0
- package/dist/lib/analytics/evidence.js +150 -0
- package/dist/lib/analytics/followthrough.js +194 -0
- package/dist/lib/analytics/forwarder.js +109 -0
- package/dist/lib/analytics/logs.js +78 -0
- package/dist/lib/analytics/metrics.js +78 -0
- package/dist/lib/analytics/recorder.js +92 -0
- package/dist/lib/analytics/review-analytics.js +75 -0
- package/dist/lib/analytics/sequence.js +77 -0
- package/dist/lib/analytics/store.js +131 -0
- package/dist/lib/analytics/turn-recap.js +279 -0
- package/dist/lib/artifact_id.js +108 -0
- package/dist/lib/auth-breaker.js +161 -0
- package/dist/lib/auto-index.js +112 -0
- package/dist/lib/classifier.js +88 -0
- package/dist/lib/config.js +298 -0
- package/dist/lib/conflict-advisory.js +64 -0
- package/dist/lib/debug-bundle.js +520 -0
- package/dist/lib/enrichment/ingest.js +301 -0
- package/dist/lib/enrichment/plan.js +253 -0
- package/dist/lib/enrichment/protocol.js +359 -0
- package/dist/lib/enrichment/scout-brief.js +176 -0
- package/dist/lib/failure-telemetry.js +444 -0
- package/dist/lib/git.js +200 -0
- package/dist/lib/governance-cache.js +77 -0
- package/dist/lib/governed-path-cache.js +76 -0
- package/dist/lib/http.js +677 -0
- package/dist/lib/identity-envelope.js +23 -0
- package/dist/lib/kb-candidate.js +65 -0
- package/dist/lib/kb_acl.js +98 -0
- package/dist/lib/login.js +353 -0
- package/dist/lib/mcp-fetchers.js +130 -0
- package/dist/lib/mcp-restart.js +47 -0
- package/dist/lib/observability.js +805 -0
- package/dist/lib/open-url.js +33 -0
- package/dist/lib/orphan-guard.js +70 -0
- package/dist/lib/packaged.js +21 -0
- package/dist/lib/reconcile-sessions.js +171 -0
- package/dist/lib/redactor.js +89 -0
- package/dist/lib/relationship-candidate-query.js +27 -0
- package/dist/lib/render.js +611 -0
- package/dist/lib/rules/applicability.js +64 -0
- package/dist/lib/rules/attest-code-rule-version.js +47 -0
- package/dist/lib/rules/attest-notes-location.js +217 -0
- package/dist/lib/rules/attest-rule-version.js +69 -0
- package/dist/lib/rules/canonical-json.js +97 -0
- package/dist/lib/rules/ce0-emit.js +64 -0
- package/dist/lib/rules/ce0-evidence.js +281 -0
- package/dist/lib/rules/ce0-recall-sample.js +82 -0
- package/dist/lib/rules/ce0-rule.js +55 -0
- package/dist/lib/rules/ce0-sampling-bucket.js +15 -0
- package/dist/lib/rules/ce0-store.js +683 -0
- package/dist/lib/rules/ce0-telemetry-project.js +93 -0
- package/dist/lib/rules/ce0-telemetry.js +158 -0
- package/dist/lib/rules/code-rule-registry.js +17 -0
- package/dist/lib/rules/command-match.js +185 -0
- package/dist/lib/rules/consult-evidence-binding.js +27 -0
- package/dist/lib/rules/consultation-capture-adapter.js +193 -0
- package/dist/lib/rules/content-match.js +56 -0
- package/dist/lib/rules/deny-admission.js +99 -0
- package/dist/lib/rules/durable-observation.js +190 -0
- package/dist/lib/rules/enforce-notes-version.js +421 -0
- package/dist/lib/rules/evaluation-input-hash.js +126 -0
- package/dist/lib/rules/evaluator.js +108 -0
- package/dist/lib/rules/inert-rule-families.js +51 -0
- package/dist/lib/rules/input-authority-resolver.js +241 -0
- package/dist/lib/rules/interception-schema.js +170 -0
- package/dist/lib/rules/interception-store.js +267 -0
- package/dist/lib/rules/live-input-authority.js +66 -0
- package/dist/lib/rules/local-matcher.js +108 -0
- package/dist/lib/rules/local-observe.js +79 -0
- package/dist/lib/rules/local-rule-version-repo.js +214 -0
- package/dist/lib/rules/memory-requirement.js +109 -0
- package/dist/lib/rules/notes-observe.js +39 -0
- package/dist/lib/rules/notes-path.js +261 -0
- package/dist/lib/rules/notes-rule.js +75 -0
- package/dist/lib/rules/observe-adapter.js +114 -0
- package/dist/lib/rules/observed-rule-hash.js +119 -0
- package/dist/lib/rules/prompt-submit-adapter.js +132 -0
- package/dist/lib/rules/requirement-subject.js +240 -0
- package/dist/lib/rules/rule-activity.js +67 -0
- package/dist/lib/rules/rule-version-hash.js +151 -0
- package/dist/lib/rules/runtime-scope.js +55 -0
- package/dist/lib/rules/stop-adapter.js +116 -0
- package/dist/lib/rules/stop-response-snapshot.js +174 -0
- package/dist/lib/rules/types.js +10 -0
- package/dist/lib/rules/ulid.js +46 -0
- package/dist/lib/rules/version-evaluation.js +156 -0
- package/dist/lib/scanner/agent-memory.js +99 -0
- package/dist/lib/scanner/bootstrap-summary.js +87 -0
- package/dist/lib/scanner/cache.js +59 -0
- package/dist/lib/scanner/frontmatter.js +42 -0
- package/dist/lib/scanner/parse-directives.js +69 -0
- package/dist/lib/scanner/parse-structured.js +72 -0
- package/dist/lib/scanner/render.js +73 -0
- package/dist/lib/scanner/scan.js +132 -0
- package/dist/lib/scanner/score.js +38 -0
- package/dist/lib/scanner/scout-mission.js +126 -0
- package/dist/lib/scanner/types.js +7 -0
- package/dist/lib/session-scope.js +195 -0
- package/dist/lib/spool.js +355 -0
- package/dist/lib/staleness.js +100 -0
- package/dist/lib/steer-cache.js +87 -0
- package/dist/lib/tagged-reference.js +20 -0
- package/dist/lib/temporal.js +109 -0
- package/dist/lib/turn-recap-emit.js +67 -0
- package/dist/lib/unwire.js +253 -0
- package/dist/lib/update-check.js +469 -0
- package/dist/lib/update-notifier.js +217 -0
- package/dist/lib/upgrade-apply.js +643 -0
- package/dist/lib/wire.js +1087 -0
- package/dist/lib/workspace.js +96 -0
- package/dist/lib/zip.js +154 -0
- package/dist/pretool-entry.js +37 -0
- package/package.json +75 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# event-batch-filter.jq: Pass 2 batch transform for flush.sh.
|
|
2
|
+
#
|
|
3
|
+
# Reads RAW newline-separated JSONL ($SESSION_ID.jsonl.draining.$$) and emits a
|
|
4
|
+
# JSON array of event records shaped for control's IngestAgentRunEventsDto.
|
|
5
|
+
#
|
|
6
|
+
# Tolerance contract (Wedge v6 Epoch 25):
|
|
7
|
+
# - ANY malformed line is silently skipped via `fromjson?`.
|
|
8
|
+
# - Empty lines are skipped via `select(length > 0)`.
|
|
9
|
+
# - One corrupt line CANNOT poison the whole batch.
|
|
10
|
+
#
|
|
11
|
+
# Pre-fix the filter used `jq -s` (slurp) which parses the entire file as a
|
|
12
|
+
# JSON-value stream. A single bad line (writer killed mid-printf, disk
|
|
13
|
+
# pressure, etc.) caused jq to exit non-zero; flush.sh's `|| echo "[]"`
|
|
14
|
+
# fallback then dropped EVERY valid event in the batch. The spool kept the
|
|
15
|
+
# raw lines only via Pass 2's re-spool on PATCH failure, but a successful
|
|
16
|
+
# PATCH-of-empty would silently advance.
|
|
17
|
+
#
|
|
18
|
+
# Invoked from flush.sh as:
|
|
19
|
+
# jq -c -R -s -f <abs-path>/event-batch-filter.jq < "$TMP"
|
|
20
|
+
#
|
|
21
|
+
# `-R` reads each line as a string; `-s` slurps the whole file into one
|
|
22
|
+
# string; `split("\n")` re-splits to lines. `fromjson?` per-line tolerates
|
|
23
|
+
# garbage.
|
|
24
|
+
#
|
|
25
|
+
# Whitelist gate (notes/20260608-agent-decision-capture-design.md section 5):
|
|
26
|
+
# an event type NOT named here is silently dropped with NO error. So
|
|
27
|
+
# `agent_decision_captured` MUST appear in the select() below or every captured
|
|
28
|
+
# agent-human decision vanishes between the spool and control, looking healthy.
|
|
29
|
+
#
|
|
30
|
+
# Transport source model (spec section 6): a captured decision carries the
|
|
31
|
+
# stronger envelope `{ source: "agent_adapter", provider, adapter }` instead of
|
|
32
|
+
# the generic `source: "claude_hook"`, so future providers do not overload
|
|
33
|
+
# `source`. `provider`/`adapter` are TRANSPORT metadata lifted to top level from
|
|
34
|
+
# the payload (`provider` / `providerSource`); control validates them for
|
|
35
|
+
# AGREEMENT with the canonical payload before writing a row
|
|
36
|
+
# (INV-ENVELOPE-PAYLOAD-CONSISTENCY), so they must mirror the payload exactly.
|
|
37
|
+
[
|
|
38
|
+
split("\n")[]
|
|
39
|
+
| select(length > 0)
|
|
40
|
+
| fromjson?
|
|
41
|
+
| select(.event == "prompt_submitted"
|
|
42
|
+
or .event == "tool_used_bash"
|
|
43
|
+
or .event == "tool_used_file"
|
|
44
|
+
or .event == "session_stopped"
|
|
45
|
+
or .event == "agent_decision_captured"
|
|
46
|
+
or .event == "injection_trace"
|
|
47
|
+
or .event == "assistant_message")
|
|
48
|
+
| if .event == "agent_decision_captured" then
|
|
49
|
+
{
|
|
50
|
+
eventKey: .eventKey,
|
|
51
|
+
eventType: .event,
|
|
52
|
+
occurredAt: .ts,
|
|
53
|
+
source: "agent_adapter",
|
|
54
|
+
provider: (.payload.provider),
|
|
55
|
+
adapter: (.payload.providerSource),
|
|
56
|
+
payload: (.payload // {})
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
{
|
|
60
|
+
eventKey: .eventKey,
|
|
61
|
+
eventType: .event,
|
|
62
|
+
occurredAt: .ts,
|
|
63
|
+
source: "claude_hook",
|
|
64
|
+
payload: (.payload // {})
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
]
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# flush.sh: background flusher. Called detached by hooks; also invokable by
|
|
3
|
+
# `mla flush`.
|
|
4
|
+
#
|
|
5
|
+
# Invariants:
|
|
6
|
+
# - Idempotent (eventKey is required + unique per runId)
|
|
7
|
+
# - Safe under concurrent hook writers via shared $SESSION_ID.lock
|
|
8
|
+
# - Orphan *.draining.* snapshots from prior interrupted flushes are
|
|
9
|
+
# recovered on every run (Correction 11) under the same lock, before
|
|
10
|
+
# detaching the active queue file
|
|
11
|
+
# - Server dedupes on (runId, eventKey); re-POSTing a recovered line is safe
|
|
12
|
+
# - finalize-session takes ONLY sessionId (Correction 6); finalMessage is
|
|
13
|
+
# persisted on the session_stopped event
|
|
14
|
+
#
|
|
15
|
+
# Source: notes/20260527-bare-bones-mvp-codebase-evaluation-and-plan.md §5.2.
|
|
16
|
+
source "$(dirname "$0")/common.sh"
|
|
17
|
+
shopt -s nullglob 2>/dev/null || true
|
|
18
|
+
|
|
19
|
+
SESSION_ID="${1:?session id required}"
|
|
20
|
+
QUEUE_FILE="$QUEUE_DIR/$SESSION_ID.jsonl"
|
|
21
|
+
LOCK="$QUEUE_DIR/$SESSION_ID.lock"
|
|
22
|
+
|
|
23
|
+
CONTROL_URL="$(jq -r '.controlUrl' "$CFG")"
|
|
24
|
+
# Bearer for control. cli-config is now nested-auth-only on disk (the top-level
|
|
25
|
+
# controlToken is a read-time projection the TS layer adds, never persisted), so
|
|
26
|
+
# read auth.accessToken first and fall back to a legacy top-level controlToken for
|
|
27
|
+
# a pre-cutover config. A logged-out config (auth.mode 'none') yields empty here;
|
|
28
|
+
# the POSTs below then 401 and the fail-soft path re-spools, as designed.
|
|
29
|
+
TOKEN="$(jq -r '.auth.accessToken // .controlToken // empty' "$CFG")"
|
|
30
|
+
# Part 3 (reactive refresh-on-401): the auth mode gates whether a 401 from a
|
|
31
|
+
# capture POST may trigger a token refresh. Only `user-token` sessions can
|
|
32
|
+
# refresh (shared-key/none have no refresh token); read it once here. Empty for a
|
|
33
|
+
# legacy config => the retry path never fires and behaviour is exactly as before.
|
|
34
|
+
AUTH_MODE="$(jq -r '.auth.mode // empty' "$CFG" 2>/dev/null || true)"
|
|
35
|
+
|
|
36
|
+
# T1.4 transport (folder = workspace): the T0.2 AgentReviewWorkspaceGuard rejects
|
|
37
|
+
# any capture write whose actor cannot be resolved, and resolveActorIdentity reads
|
|
38
|
+
# the actor ONLY from the X-Meetless-Actor header (or a body actorUserId). The TS
|
|
39
|
+
# http client (src/lib/http.ts) stamps this header on every control request, but
|
|
40
|
+
# flush.sh is the capture transport and does NOT go through that client, so it must
|
|
41
|
+
# stamp the header itself. Without it EVERY POST/PATCH below 403s ("Actor identity
|
|
42
|
+
# required") and the fail-soft path re-spools forever (capture 100% down while
|
|
43
|
+
# looking healthy). Read the actor from cli-config the same way user-prompt-submit.sh
|
|
44
|
+
# does (jq -r '.actorUserId // empty'); only stamp the header when present (an absent
|
|
45
|
+
# actor 403s exactly as before and the fail-soft path handles it).
|
|
46
|
+
ACTOR_USER_ID="$(jq -r '.actorUserId // empty' "$CFG" 2>/dev/null || true)"
|
|
47
|
+
ACTOR_HEADER=()
|
|
48
|
+
if [[ -n "${ACTOR_USER_ID:-}" ]]; then
|
|
49
|
+
ACTOR_HEADER=(-H "X-Meetless-Actor: $ACTOR_USER_ID")
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# T1.2 hard cutover (folder = workspace): the marker is the ONLY source of the
|
|
53
|
+
# workspaceId. common.sh now leaves WORKSPACE_ID empty here (flush.sh is
|
|
54
|
+
# nohup-detached with cwd=$HOME, so it MUST NOT call meetless_activated -- the
|
|
55
|
+
# walk-up would miss the repo marker). session-start.sh snapshotted the resolved
|
|
56
|
+
# marker id into this sidecar; source it so every POST below carries the marker
|
|
57
|
+
# id. Missing/empty sidecar => empty WORKSPACE_ID => the guard skips the POST.
|
|
58
|
+
WS_SIDECAR="$QUEUE_DIR/$SESSION_ID.workspaceId"
|
|
59
|
+
if [[ -s "$WS_SIDECAR" ]]; then
|
|
60
|
+
WORKSPACE_ID="$(cat "$WS_SIDECAR")"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [[ -z "${WORKSPACE_ID:-}" ]]; then
|
|
64
|
+
# No workspace resolved (no marker sidecar): nothing safe to POST. Leave queue
|
|
65
|
+
# intact for a future flush (mla doctor will warn the user).
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Correction 5 + 11: writers AND drainer use the SAME lock file. Non-blocking
|
|
70
|
+
# acquire (already-running flush exits cleanly; next hook write wakes the next
|
|
71
|
+
# flush). Orphan recovery happens INSIDE the lock, BEFORE detach, so a crash
|
|
72
|
+
# mid-POST in a previous flush cannot strand events forever.
|
|
73
|
+
exec 9>"$LOCK"
|
|
74
|
+
flock -n 9 || { log "skip: another flush already holds the session lock"; exit 0; }
|
|
75
|
+
|
|
76
|
+
# Correction 11: recover orphaned *.draining.* snapshots from prior interrupted
|
|
77
|
+
# flushes (laptop sleep, terminal close, SIGKILL, mid-POST crash). Concat back
|
|
78
|
+
# into the active queue file; the next loop below drains them. Server dedupes
|
|
79
|
+
# on (runId, eventKey), so re-POSTing already-delivered lines is safe.
|
|
80
|
+
ORPHAN_COUNT=0
|
|
81
|
+
for ORPHAN in "$QUEUE_DIR/$SESSION_ID.jsonl.draining."*; do
|
|
82
|
+
[[ -f "$ORPHAN" ]] || continue
|
|
83
|
+
cat "$ORPHAN" >> "$QUEUE_FILE"
|
|
84
|
+
rm -f "$ORPHAN"
|
|
85
|
+
ORPHAN_COUNT=$((ORPHAN_COUNT + 1))
|
|
86
|
+
done
|
|
87
|
+
if [[ "$ORPHAN_COUNT" -gt 0 ]]; then
|
|
88
|
+
log "recovered $ORPHAN_COUNT orphaned snapshot(s) from a prior interrupted flush"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Cross-session steer pull (Plan 1, conflict-resolution loop). Refresh this
|
|
92
|
+
# session's pending human steers into the local cache the UserPromptSubmit hook
|
|
93
|
+
# reads with zero network, and mark-injected any the hook already surfaced (PULLED
|
|
94
|
+
# -> INJECTED). Runs under the lock once per flush, even on an orphan-only/empty
|
|
95
|
+
# drain so an idle-but-flushing session still refreshes steers. Best-effort and
|
|
96
|
+
# time-bounded: a failure here NEVER affects the capture drain below. mla resolves
|
|
97
|
+
# its own config + auth; we only pass the session id. Gated on an executable mla
|
|
98
|
+
# (same as Pass 3).
|
|
99
|
+
if [[ -n "${MLA_PATH:-}" && -x "$MLA_PATH" ]]; then
|
|
100
|
+
# Patch 2: explicit branch over a clever parameter expansion. Hook code should be
|
|
101
|
+
# boring: wrap in `timeout`/`gtimeout` when one is on PATH, otherwise call mla
|
|
102
|
+
# directly. Either way the call is best-effort (`|| true`) so it never breaks the
|
|
103
|
+
# drain below.
|
|
104
|
+
STEER_TIMEOUT="$(command -v timeout 2>/dev/null || command -v gtimeout 2>/dev/null || true)"
|
|
105
|
+
if [[ -n "$STEER_TIMEOUT" ]]; then
|
|
106
|
+
"$STEER_TIMEOUT" 6 "$MLA_PATH" _internal steer-sync --session "$SESSION_ID" >/dev/null 2>&1 || true
|
|
107
|
+
else
|
|
108
|
+
"$MLA_PATH" _internal steer-sync --session "$SESSION_ID" >/dev/null 2>&1 || true
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# After recovery: if nothing to drain, exit cleanly.
|
|
113
|
+
if [[ ! -s "$QUEUE_FILE" ]]; then
|
|
114
|
+
log "nothing to flush (queue empty after orphan recovery)"
|
|
115
|
+
rm -f "$QUEUE_FILE"
|
|
116
|
+
exec 9>&-
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Atomic detach + truncate under lock so hook writers landing during drain
|
|
121
|
+
# go to the NEW empty file, not the snapshot being processed.
|
|
122
|
+
TMP="$QUEUE_FILE.draining.$$"
|
|
123
|
+
mv "$QUEUE_FILE" "$TMP"
|
|
124
|
+
: > "$QUEUE_FILE"
|
|
125
|
+
|
|
126
|
+
# Wedge v6 (dogfood incident 2026-06-22): collapse duplicate lines in the
|
|
127
|
+
# detached snapshot before draining, keyed by the per-event eventKey. The spool
|
|
128
|
+
# can accumulate the SAME line hundreds of thousands of times during a control
|
|
129
|
+
# outage: Pass 1/2 re-spool the failed line (spool_append), and a flush
|
|
130
|
+
# interrupted before the end-of-flush `rm -f "$TMP"` leaves a *.draining.$$
|
|
131
|
+
# orphan that the NEXT flush's orphan recovery cats straight back into the queue.
|
|
132
|
+
# Those two paths COMPOUND geometrically (~2x per interrupted cycle), so a single
|
|
133
|
+
# session_started can reach hundreds of thousands of copies (observed: a 367MB
|
|
134
|
+
# spool with 859,723 identical session_started lines). Pass 1 then fires one
|
|
135
|
+
# POST /internal/v1/agent-runs per copy -- a self-inflicted DDoS on control,
|
|
136
|
+
# every hit an idempotent no-op.
|
|
137
|
+
#
|
|
138
|
+
# The server already dedupes on (runId, eventKey), so collapsing to one line per
|
|
139
|
+
# eventKey here is loss-free and bounds EVERY pass to the count of DISTINCT
|
|
140
|
+
# events. It also caps the queue's worst case at ~2x distinct (one re-spool + one
|
|
141
|
+
# orphan re-cat) before the next detach collapses it again, so the geometric
|
|
142
|
+
# growth can never restart. Lines with no parseable eventKey are keyed by line
|
|
143
|
+
# number so malformed/unkeyed lines are NEVER collapsed away and still drain.
|
|
144
|
+
# DEDUP_TMP sits in $QUEUE_DIR for an atomic same-filesystem rename, but is named
|
|
145
|
+
# OUTSIDE the *.jsonl.draining.* orphan-recovery glob (see the Pass 1 mktemp
|
|
146
|
+
# note) so a crash mid-dedup can never re-inject it as a bogus queue line. If awk
|
|
147
|
+
# is missing or errors, we drain the snapshot unchanged -- server-side
|
|
148
|
+
# (runId, eventKey) dedup still guarantees correctness, just without the cap.
|
|
149
|
+
DEDUP_TMP="$QUEUE_DIR/$SESSION_ID.dedup.$$"
|
|
150
|
+
if awk '{
|
|
151
|
+
if (match($0, /"eventKey":"[^"]*"/)) key = substr($0, RSTART, RLENGTH);
|
|
152
|
+
else key = "__nokey__" NR;
|
|
153
|
+
if (!seen[key]++) print
|
|
154
|
+
}' "$TMP" > "$DEDUP_TMP" 2>/dev/null; then
|
|
155
|
+
mv -f "$DEDUP_TMP" "$TMP"
|
|
156
|
+
else
|
|
157
|
+
rm -f "$DEDUP_TMP"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
log "draining $(wc -l < "$TMP" 2>/dev/null | tr -d ' ' || echo '?') queued line(s) -> $CONTROL_URL"
|
|
161
|
+
|
|
162
|
+
# Release the lock once the snapshot is detached. Hook writers can append
|
|
163
|
+
# concurrently while we POST.
|
|
164
|
+
exec 9>&-
|
|
165
|
+
|
|
166
|
+
HAS_FINALIZE=0
|
|
167
|
+
EVENTS_OK=1
|
|
168
|
+
# Set to 1 only after a successful finalize (Pass 3 OK branch). Gates the
|
|
169
|
+
# end-of-flush full-sidecar reap below: an ended, cleanly-finalized session has
|
|
170
|
+
# no future turn, so its .turn/.lock/.hb*/.narration-cursor* sidecars are litter
|
|
171
|
+
# we reclaim now instead of leaving 6 files for the 24h age-gated reaper.
|
|
172
|
+
FINALIZE_OK=0
|
|
173
|
+
|
|
174
|
+
# Process the snapshot in two passes:
|
|
175
|
+
# Pass 1: session_started lines -> POST /internal/v1/agent-runs (one per line).
|
|
176
|
+
# Pass 2: prompt_submitted | tool_used_bash | tool_used_file | session_stopped
|
|
177
|
+
# | agent_decision_captured | injection_trace | assistant_message lines
|
|
178
|
+
# -> batched PATCH
|
|
179
|
+
# /internal/v1/agent-runs/by-session/:sid/events. The forward whitelist
|
|
180
|
+
# (event-batch-filter.jq) and the re-spool whitelist below MUST list
|
|
181
|
+
# the same types or a type forwards on success but vanishes on retry.
|
|
182
|
+
# finalize_requested is a control signal only; it never POSTs by itself, it
|
|
183
|
+
# triggers the `mla _internal finalize-session` hop at the end. The server
|
|
184
|
+
# dedupes on (runId, eventKey), so re-POSTing a batch on retry is safe.
|
|
185
|
+
#
|
|
186
|
+
# Each raw JSONL line is shaped `{ts, event, eventKey, sessionId, payload}`.
|
|
187
|
+
# Control's DTOs require Nest-flavored shapes (CreateAgentRunDto +
|
|
188
|
+
# IngestAgentRunEventsDto). The jq transforms below map fields:
|
|
189
|
+
# ts -> startedAt | occurredAt
|
|
190
|
+
# event -> eventType
|
|
191
|
+
# sessionId -> externalSessionId
|
|
192
|
+
# payload -> kept as-is + selected fields lifted to top level
|
|
193
|
+
# All POSTs include workspaceId, sourced from the per-session .workspaceId
|
|
194
|
+
# sidecar (the marker id snapshotted at session start). See T1.2 cutover above.
|
|
195
|
+
#
|
|
196
|
+
# curl keeps `-f` so HTTP 4xx/5xx are still failures (exit 22) that re-spool;
|
|
197
|
+
# a control-side validation error or 5xx is never silently swallowed. We ALSO add
|
|
198
|
+
# `-w '%{http_code}'` (which curl prints even when `-f` fails) so the specific
|
|
199
|
+
# auth/visibility codes 401 / 403 / 404 can fail SOFT: on failure the flusher
|
|
200
|
+
# fires a throttled local warning via warn_capture_auth (common.sh) instead of
|
|
201
|
+
# going silent, because "committed marker, token not yet a workspace member"
|
|
202
|
+
# (403) is a common transient onboarding state. Success/failure still branches on
|
|
203
|
+
# curl's exit code (CURL_RC), so the contract is unchanged for every other code;
|
|
204
|
+
# the status code only selects whether to warn. A transport error yields code 000
|
|
205
|
+
# (no warn) and re-spools. The flusher never blocks the session (always exits 0).
|
|
206
|
+
|
|
207
|
+
# control_capture_curl METHOD URL BODY: perform a capture write with the current
|
|
208
|
+
# $TOKEN, then (Part 3 §B reactive refresh-on-401) recover a single expired-access
|
|
209
|
+
# token. Sets globals HTTP_CODE (the %{http_code}) and CURL_RC (curl's exit code)
|
|
210
|
+
# for the caller to branch on, EXACTLY as the inline curl did before, so the
|
|
211
|
+
# success/fail/warn/re-spool contract downstream is unchanged. The only added
|
|
212
|
+
# behaviour: when the first attempt fails with HTTP 401 AND the session is
|
|
213
|
+
# `auth.mode == user-token`, fire one synchronous `mla _internal refresh`
|
|
214
|
+
# (refresh_user_token, common.sh). On rc 0 (token rotated) re-read the rotated
|
|
215
|
+
# access token and retry the SAME request EXACTLY ONCE. Any other refresh outcome
|
|
216
|
+
# (busy 75 / expired 77 / not-attempted 70 / wrong-mode 64) leaves the original
|
|
217
|
+
# failure in place for the caller's existing fail-soft path. One-shot, never a
|
|
218
|
+
# loop: a still-401 retry is returned as-is. set -e-safe via `|| CURL_RC=$?` and
|
|
219
|
+
# `|| refresh_rc=$?` (this whole script runs under common.sh's set -euo pipefail).
|
|
220
|
+
control_capture_curl() {
|
|
221
|
+
# body_file, not an inline body string. A busy session's Pass 2 events[]
|
|
222
|
+
# serializes to ~1-2 MB; passing that on the curl ARGV (`--data "$body"`)
|
|
223
|
+
# overflowed execve (E2BIG) and aborted the whole flush under `set -e`
|
|
224
|
+
# (dogfood incident 2026-06-11). `--data-binary @<file>` streams the bytes
|
|
225
|
+
# verbatim off-argv, so body size is bounded only by control's 10mb body
|
|
226
|
+
# limit, never by ARG_MAX. -binary (not plain --data) so curl does not strip
|
|
227
|
+
# newlines/CRs from the file.
|
|
228
|
+
local method="$1" url="$2" body_file="$3"
|
|
229
|
+
HTTP_CODE=000
|
|
230
|
+
CURL_RC=0
|
|
231
|
+
HTTP_CODE="$(curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' \
|
|
232
|
+
-X "$method" "$url" \
|
|
233
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
234
|
+
"${ACTOR_HEADER[@]+"${ACTOR_HEADER[@]}"}" \
|
|
235
|
+
--data-binary @"$body_file" 2>/dev/null)" || CURL_RC=$?
|
|
236
|
+
if [[ "$CURL_RC" -ne 0 && "$HTTP_CODE" == "401" && "$AUTH_MODE" == "user-token" ]]; then
|
|
237
|
+
local refresh_rc=0
|
|
238
|
+
refresh_user_token || refresh_rc=$?
|
|
239
|
+
if [[ "$refresh_rc" -eq 0 ]]; then
|
|
240
|
+
TOKEN="$(jq -r '.auth.accessToken // .controlToken // empty' "$CFG" 2>/dev/null || true)"
|
|
241
|
+
log "flush: control 401 on $method; refreshed access token, retrying once"
|
|
242
|
+
HTTP_CODE=000
|
|
243
|
+
CURL_RC=0
|
|
244
|
+
HTTP_CODE="$(curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' \
|
|
245
|
+
-X "$method" "$url" \
|
|
246
|
+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
|
247
|
+
"${ACTOR_HEADER[@]+"${ACTOR_HEADER[@]}"}" \
|
|
248
|
+
--data-binary @"$body_file" 2>/dev/null)" || CURL_RC=$?
|
|
249
|
+
else
|
|
250
|
+
log "flush: control 401 on $method; refresh did not rotate a token (rc=$refresh_rc); failing soft"
|
|
251
|
+
fi
|
|
252
|
+
fi
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Pass 1: session_started -> create AgentRun. There is normally exactly one
|
|
256
|
+
# such line per session; if multiple slip in (re-attached session), the
|
|
257
|
+
# server's upsert on (workspaceId, adapter, externalSessionId) keeps it
|
|
258
|
+
# idempotent so re-POST is safe.
|
|
259
|
+
while IFS= read -r LINE || [[ -n "$LINE" ]]; do
|
|
260
|
+
[[ -z "$LINE" ]] && continue
|
|
261
|
+
# Wedge v6 Epoch 26: tolerate malformed lines. Pre-fix the unguarded
|
|
262
|
+
# `EVT="$(... | jq -r '.event')"` propagated jq's non-zero exit through
|
|
263
|
+
# `set -euo pipefail`, crashing the whole flush. The .draining.$$ snapshot
|
|
264
|
+
# would be stranded; the next flush's orphan recovery re-cats the same
|
|
265
|
+
# bad line and crashes again -- infinite reflush loop on one corrupt
|
|
266
|
+
# write. The `|| echo ""` inside the subshell pins the failure inside the
|
|
267
|
+
# subshell so EVT becomes "" and the loop continues.
|
|
268
|
+
EVT="$(printf '%s' "$LINE" | jq -r '.event' 2>/dev/null || echo "")"
|
|
269
|
+
if [[ "$EVT" != "session_started" ]]; then
|
|
270
|
+
continue
|
|
271
|
+
fi
|
|
272
|
+
BODY="$(printf '%s' "$LINE" | jq -c --arg ws "$WORKSPACE_ID" \
|
|
273
|
+
'{workspaceId: $ws,
|
|
274
|
+
externalSessionId: .sessionId,
|
|
275
|
+
adapter: (.payload.adapter // "claude_code"),
|
|
276
|
+
repoPath: (.payload.repoPath // ""),
|
|
277
|
+
branch: (.payload.branch // null),
|
|
278
|
+
startedAt: .ts}
|
|
279
|
+
| with_entries(select(.value != null))' 2>/dev/null || echo "")"
|
|
280
|
+
if [[ -z "$BODY" ]]; then
|
|
281
|
+
continue
|
|
282
|
+
fi
|
|
283
|
+
# Stream the body from a file (printf is a shell builtin, no argv limit) so
|
|
284
|
+
# the transport is uniform with Pass 2 and never grows onto curl's ARGV. Use
|
|
285
|
+
# mktemp (system tmp), NOT a "$TMP.*" name: $TMP is "$QUEUE_FILE.draining.$$",
|
|
286
|
+
# so any "$TMP.*" child matches the orphan-recovery glob ("*.jsonl.draining.*")
|
|
287
|
+
# and would be cat'd back into the queue as a bogus event line.
|
|
288
|
+
P1_BODY_FILE="$(mktemp "${TMPDIR:-/tmp}/mla-p1body.XXXXXX")"
|
|
289
|
+
printf '%s' "$BODY" > "$P1_BODY_FILE"
|
|
290
|
+
control_capture_curl POST "$CONTROL_URL/internal/v1/agent-runs" "$P1_BODY_FILE"
|
|
291
|
+
rm -f "$P1_BODY_FILE"
|
|
292
|
+
if [[ "$CURL_RC" -eq 0 ]]; then
|
|
293
|
+
log "Pass 1: created/updated agent run (POST /internal/v1/agent-runs)"
|
|
294
|
+
else
|
|
295
|
+
case "$HTTP_CODE" in
|
|
296
|
+
401|403|404) warn_capture_auth "$SESSION_ID" "$HTTP_CODE" "POST /internal/v1/agent-runs" ;;
|
|
297
|
+
esac
|
|
298
|
+
log "Pass 1: POST /internal/v1/agent-runs FAILED (HTTP $HTTP_CODE; control unreachable or 4xx/5xx); re-spooled session_started for retry"
|
|
299
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
300
|
+
continue
|
|
301
|
+
fi
|
|
302
|
+
done < "$TMP"
|
|
303
|
+
|
|
304
|
+
# Pass 2: collect every non-session_started event-bearing line into a single
|
|
305
|
+
# events[] array and PATCH once. eventKey on each line dedupes server-side, so
|
|
306
|
+
# retries are safe. Empty events array short-circuits the PATCH.
|
|
307
|
+
#
|
|
308
|
+
# Wedge v6 Epoch 25: the filter lives in `event-batch-filter.jq` next to this
|
|
309
|
+
# script so a corrupt-line tolerance contract can be unit-tested. Pre-fix
|
|
310
|
+
# `jq -s` failed the whole batch on ONE malformed line and `|| echo "[]"`
|
|
311
|
+
# silently dropped every valid event with it. The new filter uses
|
|
312
|
+
# `-R -s` + `fromjson?` to skip just the bad line.
|
|
313
|
+
#
|
|
314
|
+
# Wedge v6 Epoch 32: distinguish "filter ran and returned []" (genuinely
|
|
315
|
+
# empty batch, OK to short-circuit) from "filter file missing OR jq crashed"
|
|
316
|
+
# (we have no visibility into the snapshot and MUST NOT let Pass 3 burn the
|
|
317
|
+
# `agent_run_finalized:<runId>` outbox idempotency key on an empty event
|
|
318
|
+
# set). Pre-Epoch-32 the `|| echo "[]"` fallback collapsed both into
|
|
319
|
+
# EVENTS_OK=1 + finalize-session firing on a Run Ledger with no bash events
|
|
320
|
+
# and no agentClaimsRaw. Subsequent flushes (after re-installing the filter)
|
|
321
|
+
# would have nothing to re-deliver: dedupe wins, worker synthesizes a blank
|
|
322
|
+
# review packet, total silent loss. Now: filter failure -> EVENTS_OK=0 ->
|
|
323
|
+
# re-spool block below replays the events AND Pass 3 re-spools the finalize
|
|
324
|
+
# instead of firing it.
|
|
325
|
+
# Wedge v6 (dogfood incident 2026-06-11): build the batch entirely through
|
|
326
|
+
# files, never shell-variable-to-argv. A busy session's events[] is multi-MB;
|
|
327
|
+
# the pre-fix `--argjson events "$EVENTS_JSON"` put that array on jq's ARGV and
|
|
328
|
+
# overflowed execve (E2BIG), aborting the flush under `set -e` before any curl.
|
|
329
|
+
# The filter output goes straight to $EVENTS_FILE; the request body is assembled
|
|
330
|
+
# with `--slurpfile` (jq reads the file, nothing on argv); curl streams the body
|
|
331
|
+
# file. EVENTS_OK semantics (missing filter / jq crash -> defer + re-spool) are
|
|
332
|
+
# preserved exactly.
|
|
333
|
+
EVENT_FILTER="$(dirname "$0")/event-batch-filter.jq"
|
|
334
|
+
EVENTS_FILE=""
|
|
335
|
+
EVENT_COUNT=0
|
|
336
|
+
if [[ ! -f "$EVENT_FILTER" ]]; then
|
|
337
|
+
EVENTS_OK=0
|
|
338
|
+
log "Pass 2: event-batch-filter.jq MISSING; deferring events + finalize (run mla init to repair hooks)"
|
|
339
|
+
else
|
|
340
|
+
# mktemp (system tmp), NOT "$TMP.*": see the Pass 1 note. The filter output
|
|
341
|
+
# streams straight to the file; the array never touches a shell var or argv.
|
|
342
|
+
EVENTS_FILE="$(mktemp "${TMPDIR:-/tmp}/mla-events.XXXXXX")"
|
|
343
|
+
if ! jq -c -R -s -f "$EVENT_FILTER" < "$TMP" > "$EVENTS_FILE" 2>/dev/null; then
|
|
344
|
+
EVENTS_OK=0
|
|
345
|
+
log "Pass 2: jq event filter crashed; deferring events + finalize"
|
|
346
|
+
else
|
|
347
|
+
EVENT_COUNT="$(jq 'length' < "$EVENTS_FILE" 2>/dev/null || echo 0)"
|
|
348
|
+
fi
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
if [[ "${EVENT_COUNT:-0}" -gt 0 ]]; then
|
|
352
|
+
# --slurpfile wraps the file's single JSON array value as $evs[0]. No argv
|
|
353
|
+
# carries the payload, so this is overflow-proof regardless of batch size.
|
|
354
|
+
PATCH_BODY_FILE="$(mktemp "${TMPDIR:-/tmp}/mla-body.XXXXXX")"
|
|
355
|
+
jq -c -n --arg ws "$WORKSPACE_ID" --slurpfile evs "$EVENTS_FILE" \
|
|
356
|
+
'{workspaceId: $ws, events: $evs[0]}' > "$PATCH_BODY_FILE" 2>/dev/null
|
|
357
|
+
control_capture_curl PATCH \
|
|
358
|
+
"$CONTROL_URL/internal/v1/agent-runs/by-session/$SESSION_ID/events" "$PATCH_BODY_FILE"
|
|
359
|
+
rm -f "$PATCH_BODY_FILE"
|
|
360
|
+
if [[ "$CURL_RC" -eq 0 ]]; then
|
|
361
|
+
log "Pass 2: PATCHed $EVENT_COUNT event(s) -> /by-session/$SESSION_ID/events"
|
|
362
|
+
else
|
|
363
|
+
# Non-2xx (control down, transient network, HTTP 4xx/5xx). Server dedupes on
|
|
364
|
+
# eventKey, so the re-spool block below replays the lot. 401/403/404 also fire
|
|
365
|
+
# a throttled local warning (fail soft); other codes just re-spool silently.
|
|
366
|
+
case "$HTTP_CODE" in
|
|
367
|
+
401|403|404) warn_capture_auth "$SESSION_ID" "$HTTP_CODE" "PATCH /internal/v1/agent-runs/by-session/$SESSION_ID/events" ;;
|
|
368
|
+
esac
|
|
369
|
+
EVENTS_OK=0
|
|
370
|
+
log "Pass 2: PATCH events FAILED (HTTP $HTTP_CODE; control unreachable or 4xx/5xx); will re-spool $EVENT_COUNT event(s)"
|
|
371
|
+
fi
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
# The events scratch file has served its purpose (body already built and sent).
|
|
375
|
+
# The re-spool path below reads from $TMP, never from EVENTS_FILE. Explicit `if`,
|
|
376
|
+
# not `[[ ]] && rm`, so a falsy guard does not trip `set -e`.
|
|
377
|
+
if [[ -n "$EVENTS_FILE" ]]; then
|
|
378
|
+
rm -f "$EVENTS_FILE"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# Wedge v6 Epoch 32: re-spool every event-bearing line on ANY Pass 2 failure
|
|
382
|
+
# (filter file missing, jq crashed, OR PATCH failed). One code path serves
|
|
383
|
+
# all three failure modes; pre-Epoch-32 only the PATCH-failure path replayed,
|
|
384
|
+
# so a missing filter stranded the batch and let finalize ship empty.
|
|
385
|
+
if [[ "$EVENTS_OK" == "0" ]]; then
|
|
386
|
+
log "re-spooling event-bearing line(s) for the next flush"
|
|
387
|
+
while IFS= read -r LINE || [[ -n "$LINE" ]]; do
|
|
388
|
+
[[ -z "$LINE" ]] && continue
|
|
389
|
+
# Wedge v6 Epoch 26: same tolerance as Pass 1. A malformed line in the
|
|
390
|
+
# snapshot would crash this re-spool loop under `set -e` and strand the
|
|
391
|
+
# batch in .draining.$$ permanently.
|
|
392
|
+
EVT="$(printf '%s' "$LINE" | jq -r '.event' 2>/dev/null || echo "")"
|
|
393
|
+
case "$EVT" in
|
|
394
|
+
prompt_submitted|tool_used_bash|tool_used_file|session_stopped|agent_decision_captured|injection_trace|assistant_message)
|
|
395
|
+
spool_append "$SESSION_ID" "$LINE"
|
|
396
|
+
;;
|
|
397
|
+
esac
|
|
398
|
+
done < "$TMP"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# Pass 3: detect finalize_requested. Always last because finalize triggers the
|
|
402
|
+
# review packet pipeline; everything else must be persisted server-side first.
|
|
403
|
+
# If Pass 2 failed (EVENTS_OK=0), DO NOT fire finalize now -- the worker would
|
|
404
|
+
# build a Run Ledger from a partially-persisted event set (e.g. missing
|
|
405
|
+
# session_stopped -> agentClaimsRaw=null), and the outbox idempotencyKey
|
|
406
|
+
# `agent_run_finalized:<runId>` is unique, so a later retry would silently
|
|
407
|
+
# dedupe instead of re-synthesizing. Re-spool finalize_requested so the next
|
|
408
|
+
# flush (after events land) fires it cleanly.
|
|
409
|
+
if grep -q '"event":"finalize_requested"' "$TMP" 2>/dev/null; then
|
|
410
|
+
if [[ "$EVENTS_OK" == "0" ]]; then
|
|
411
|
+
log "Pass 3: finalize_requested DEFERRED (events not yet persisted); re-spooled for next flush"
|
|
412
|
+
FALLBACK_KEY="$(gen_event_key)"
|
|
413
|
+
spool_append "$SESSION_ID" "$(jq -c -n --arg sessionId "$SESSION_ID" --arg key "$FALLBACK_KEY" \
|
|
414
|
+
'{event:"finalize_requested", eventKey:$key, sessionId:$sessionId, payload:{}}')"
|
|
415
|
+
else
|
|
416
|
+
HAS_FINALIZE=1
|
|
417
|
+
fi
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
if [[ "$HAS_FINALIZE" == "1" ]]; then
|
|
421
|
+
# Correction 6: finalize takes ONLY sessionId. finalMessage is on the
|
|
422
|
+
# persisted session_stopped event. Correction 7: absolute MLA_PATH.
|
|
423
|
+
#
|
|
424
|
+
# Wedge v6 Epoch 35: export MEETLESS_REPO_PATH from the session-start
|
|
425
|
+
# sidecar BEFORE invoking finalize. flush.sh is `nohup`-spawned, so its
|
|
426
|
+
# cwd is whatever nohup ran in (often $HOME). The CLI's Epoch 33 guard
|
|
427
|
+
# refuses to POST when captureGitEvidence(cwd) returns empty topLevel,
|
|
428
|
+
# so without this export every finalize attempt re-spools and the next
|
|
429
|
+
# flush re-fails the same way -- permanent stuck-loss. The sidecar holds
|
|
430
|
+
# the repo path Claude Code fired the SessionStart hook with (the real
|
|
431
|
+
# project root). Missing sidecar is tolerated; CLI then falls back to
|
|
432
|
+
# process.cwd() and likely refuses, which is the correct "loud" signal.
|
|
433
|
+
REPO_SIDECAR="$QUEUE_DIR/$SESSION_ID.repoPath"
|
|
434
|
+
if [[ -s "$REPO_SIDECAR" ]]; then
|
|
435
|
+
export MEETLESS_REPO_PATH="$(cat "$REPO_SIDECAR")"
|
|
436
|
+
fi
|
|
437
|
+
if [[ -z "${MLA_PATH:-}" || ! -x "$MLA_PATH" ]]; then
|
|
438
|
+
log "Pass 3: finalize SKIPPED (mla CLI not executable at MLA_PATH); re-spooled finalize_requested"
|
|
439
|
+
FALLBACK_KEY="$(gen_event_key)"
|
|
440
|
+
spool_append "$SESSION_ID" "$(jq -c -n --arg sessionId "$SESSION_ID" --arg key "$FALLBACK_KEY" \
|
|
441
|
+
'{event:"finalize_requested", eventKey:$key, sessionId:$sessionId, payload:{}}')"
|
|
442
|
+
else
|
|
443
|
+
log "Pass 3: finalizing session (mla _internal finalize-session) -> triggers review packet pipeline"
|
|
444
|
+
if "$MLA_PATH" _internal finalize-session "$SESSION_ID"; then
|
|
445
|
+
log "Pass 3: finalize OK; review packet pipeline triggered. Inspect: mla review (inside session $SESSION_ID) | raw turns: mla session show $SESSION_ID"
|
|
446
|
+
# Successful finalize: drop the sidecars so a future stray flush
|
|
447
|
+
# for this sessionId doesn't keep exporting a stale path or
|
|
448
|
+
# subtracting a stale baseline. Preserved on failure below so a
|
|
449
|
+
# retry still attributes only session-touched changes.
|
|
450
|
+
rm -f "$REPO_SIDECAR"
|
|
451
|
+
rm -f "$QUEUE_DIR/$SESSION_ID.gitBaseline"
|
|
452
|
+
rm -f "$QUEUE_DIR/$SESSION_ID.workspaceId"
|
|
453
|
+
FINALIZE_OK=1
|
|
454
|
+
else
|
|
455
|
+
log "Pass 3: finalize FAILED; re-spooled finalize_requested for next flush"
|
|
456
|
+
FALLBACK_KEY="$(gen_event_key)"
|
|
457
|
+
spool_append "$SESSION_ID" "$(jq -c -n --arg sessionId "$SESSION_ID" --arg key "$FALLBACK_KEY" \
|
|
458
|
+
'{event:"finalize_requested", eventKey:$key, sessionId:$sessionId, payload:{}}')"
|
|
459
|
+
fi
|
|
460
|
+
fi
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
log "flush complete"
|
|
464
|
+
rm -f "$TMP"
|
|
465
|
+
|
|
466
|
+
# RC1 (self-clean): a cleanly-drained session leaves QUEUE_FILE as the 0-byte
|
|
467
|
+
# file we truncated at detach time. Pre-fix it was removed ONLY at the top of a
|
|
468
|
+
# SUBSEQUENT flush (the "nothing to flush" branch above) -- but stop.sh fires
|
|
469
|
+
# the LAST flush of an ended session and no subsequent flush ever comes, so the
|
|
470
|
+
# empty spool lingered and queueDepth() counted it as an "active session"
|
|
471
|
+
# forever (the phantom "N active sessions" mla doctor reported). Remove it here,
|
|
472
|
+
# under the SAME lock spool_append uses, but ONLY when it is still empty: Pass
|
|
473
|
+
# 1/2/3 re-spool on failure and a concurrent next-turn spool_append may have
|
|
474
|
+
# appended, both of which make it non-empty and MUST be preserved. spool_append
|
|
475
|
+
# recreates it with `>>` next turn, so removal is safe for a live session too.
|
|
476
|
+
# Non-blocking acquire: if a writer or another flush holds the lock we skip and
|
|
477
|
+
# the next flush's top-of-flush empty check is the backstop. The .lock/.turn
|
|
478
|
+
# sidecars are left for the age-gated `mla flush --gc` reaper (reapQueue), since
|
|
479
|
+
# a live multi-turn session still needs its turn counter.
|
|
480
|
+
exec 9>"$LOCK"
|
|
481
|
+
if flock -n 9; then
|
|
482
|
+
[[ -s "$QUEUE_FILE" ]] || rm -f "$QUEUE_FILE"
|
|
483
|
+
exec 9>&-
|
|
484
|
+
fi
|
|
485
|
+
|
|
486
|
+
# RC2 (finalize-time full reap): a cleanly-finalized session is OVER -- stop.sh
|
|
487
|
+
# fires its last flush and no later flush ever comes. The self-clean above only
|
|
488
|
+
# removes the empty spool, and the Pass 3 OK branch only the .repoPath/
|
|
489
|
+
# .gitBaseline/.workspaceId sidecars; the .turn (turn counter), .lock, .hb +
|
|
490
|
+
# .hb.lock (heartbeat) and .narration-cursor + .narration-cursor.lock sidecars
|
|
491
|
+
# were left for the 24h age-gated reaper -- 6 stranded files per ended session.
|
|
492
|
+
# Reclaim them now, but ONLY when the spool actually drained empty: a re-spooled
|
|
493
|
+
# finalize/retry tail (QUEUE_FILE still present and non-empty) means the session
|
|
494
|
+
# is not truly done, so we keep everything for the next flush. The fd-9 lock was
|
|
495
|
+
# already released above; a finalized session has no concurrent writer.
|
|
496
|
+
if [[ "$FINALIZE_OK" == "1" && ! -s "$QUEUE_FILE" ]]; then
|
|
497
|
+
rm -f "$QUEUE_DIR/$SESSION_ID.turn"
|
|
498
|
+
rm -f "$QUEUE_DIR/$SESSION_ID.hb" "$QUEUE_DIR/$SESSION_ID.hb.lock"
|
|
499
|
+
rm -f "$QUEUE_DIR/$SESSION_ID.narration-cursor" "$QUEUE_DIR/$SESSION_ID.narration-cursor.lock"
|
|
500
|
+
rm -f "$LOCK"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
exit 0
|