create-egregore 0.21.0-runtime-mvp.13 → 0.21.0-runtime-mvp.15
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/assets/egregore-launcher.js +7 -3
- package/package.json +1 -1
- package/runtime/claude/bin/lib/greeting.sh +3 -7
- package/runtime/claude/bin/lib/settings-drift.sh +5 -1
- package/runtime/claude/bin/search.sh +37 -10
- package/runtime/claude/egregore_runtime/adapters/qmd.py +42 -6
- package/runtime/claude/egregore_runtime/harness_cli.py +6 -0
- package/runtime/claude/egregore_runtime/upgrade.py +64 -2
- package/runtime/codex/bin/lib/greeting.sh +3 -7
- package/runtime/codex/bin/lib/settings-drift.sh +5 -1
- package/runtime/codex/bin/search.sh +37 -10
- package/runtime/codex/egregore_runtime/adapters/qmd.py +42 -6
- package/runtime/codex/egregore_runtime/harness_cli.py +6 -0
- package/runtime/codex/egregore_runtime/upgrade.py +64 -2
- package/runtime/pi/bin/lib/greeting.sh +3 -7
- package/runtime/pi/bin/lib/settings-drift.sh +5 -1
- package/runtime/pi/bin/search.sh +37 -10
- package/runtime/pi/egregore_runtime/adapters/qmd.py +42 -6
- package/runtime/pi/egregore_runtime/harness_cli.py +6 -0
- package/runtime/pi/egregore_runtime/upgrade.py +64 -2
- package/runtime/prime/bin/lib/greeting.sh +3 -7
- package/runtime/prime/bin/lib/settings-drift.sh +5 -1
- package/runtime/prime/bin/search.sh +37 -10
- package/runtime/prime/egregore_runtime/adapters/qmd.py +42 -6
- package/runtime/prime/egregore_runtime/harness_cli.py +6 -0
- package/runtime/prime/egregore_runtime/upgrade.py +64 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
2
|
+
// egregore-launcher-version: 0.21.0-runtime-mvp.15
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.0-runtime-mvp.
|
|
38
|
+
const VERSION = "0.21.0-runtime-mvp.15";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -2673,7 +2673,11 @@ function formatUpgradeLines(inst, st, { candidate = "", retrieval = null } = {})
|
|
|
2673
2673
|
+ (d.candidate_version ? ` → candidate ${d.candidate_version}` : candidate ? ` → candidate ${candidate} available` : ""));
|
|
2674
2674
|
lines.push(`Status: ${UPGRADE_STATUS_LABEL[d.status] || d.status}`);
|
|
2675
2675
|
if (d.candidate_digest) lines.push(`Package digest: sha256 ${String(d.candidate_digest).slice(0, 16)}…`);
|
|
2676
|
-
|
|
2676
|
+
// A recorded failure only needs attention while the transaction is stuck
|
|
2677
|
+
// on it; once a later prepare succeeds the line would contradict "ready".
|
|
2678
|
+
if (d.failure && d.failure.message && (d.status === "failed" || d.status === "interrupted")) {
|
|
2679
|
+
lines.push(`Attention: ${d.failure.message}`);
|
|
2680
|
+
}
|
|
2677
2681
|
if (d.rollback_version && d.status !== "none") lines.push(`Rollback target: ${d.rollback_version}`);
|
|
2678
2682
|
const ts = d.team_sync;
|
|
2679
2683
|
if (ts && ts.label) lines.push(`Team memory: ${ts.label}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.21.0-runtime-mvp.
|
|
3
|
+
"version": "0.21.0-runtime-mvp.15",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -95,19 +95,15 @@ if [ -n "$SAVED_BRANCH" ]; then
|
|
|
95
95
|
echo " ✓ Auto-saved uncommitted work on $SAVED_BRANCH"
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
-
# ---
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fi
|
|
98
|
+
# --- Org-settings sync receipts. A settings.json heal is logged to
|
|
99
|
+
# .egregore/settings-drift.log, never announced; local settings changes reach
|
|
100
|
+
# the control plane through bin/settings.sh, so there is nothing to ask. ---
|
|
102
101
|
if [ -n "${SETTINGS_SYNCED_FROM_ORG:-}" ]; then
|
|
103
102
|
echo " ◇ org settings synced from the control plane (${SETTINGS_SYNCED_FROM_ORG})"
|
|
104
103
|
fi
|
|
105
104
|
if [ -n "${SETTINGS_POSTURE_HELD:-}" ]; then
|
|
106
105
|
echo " ◇ boundary posture was set to ${SETTINGS_POSTURE_HELD} elsewhere — relaxing access needs your confirm: bash bin/settings.sh posture ${SETTINGS_POSTURE_HELD}"
|
|
107
106
|
fi
|
|
108
|
-
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
109
|
-
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
110
|
-
fi
|
|
111
107
|
|
|
112
108
|
# --- Footer: health + repos + memory (tertiary) ---
|
|
113
109
|
echo "$SEPARATOR"
|
|
@@ -61,7 +61,11 @@ settings_drift_heal() {
|
|
|
61
61
|
|
|
62
62
|
if ! git diff --quiet -- .claude/settings.json 2>/dev/null; then
|
|
63
63
|
if _settings_is_reduced_adapter; then
|
|
64
|
-
git checkout -- .claude/settings.json 2>/dev/null
|
|
64
|
+
if git checkout -- .claude/settings.json 2>/dev/null; then
|
|
65
|
+
SETTINGS_ADAPTER_RESTORED="true"
|
|
66
|
+
mkdir -p .egregore 2>/dev/null && printf '%s restored .claude/settings.json: a generated adapter had replaced the org file\n' \
|
|
67
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> .egregore/settings-drift.log 2>/dev/null || true
|
|
68
|
+
fi
|
|
65
69
|
fi
|
|
66
70
|
fi
|
|
67
71
|
|
|
@@ -32,6 +32,19 @@ _maintenance_runtime() {
|
|
|
32
32
|
python3 -m egregore_runtime.adapters.qmd_cli "$@"
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
_now_ms() {
|
|
36
|
+
if command -v perl >/dev/null 2>&1; then
|
|
37
|
+
perl -MTime::HiRes=time -e 'printf("%d\n", time() * 1000)'
|
|
38
|
+
else
|
|
39
|
+
echo $(( $(date +%s) * 1000 ))
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_elapsed_s() {
|
|
44
|
+
# One-decimal seconds since a _now_ms() timestamp.
|
|
45
|
+
awk -v ms="$(( $(_now_ms) - $1 ))" 'BEGIN { printf "%.1f", ms / 1000 }'
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
_result_paths() {
|
|
36
49
|
local line path canonical_prefix
|
|
37
50
|
canonical_prefix="**file:** \`memory/"
|
|
@@ -125,14 +138,14 @@ _banner_and_emit() {
|
|
|
125
138
|
actual="${actual%% *}"
|
|
126
139
|
required="${gap#*required=}"
|
|
127
140
|
required="${required%% *}"
|
|
128
|
-
dur=$(
|
|
141
|
+
dur=$(_elapsed_s "$t0")
|
|
129
142
|
printf '⌕ Egregore · searching your organization’s memory · %s · evidence gap · %s/%s sources · %ss\n\n' \
|
|
130
143
|
"$label" "$actual" "$required" "$dur"
|
|
131
144
|
[ "$hide_results" = 1 ] || printf '%s\n' "$out" | tail -n +2
|
|
132
145
|
return 0
|
|
133
146
|
fi
|
|
134
147
|
hits=$(printf '%s\n' "$out" | _result_paths | wc -l | tr -d ' ')
|
|
135
|
-
dur=$(
|
|
148
|
+
dur=$(_elapsed_s "$t0")
|
|
136
149
|
product="Egregore"
|
|
137
150
|
surface="searching your organization’s memory"
|
|
138
151
|
if [ "$emit" = "_enrich" ] && [ "$hits" -gt 0 ]; then
|
|
@@ -223,7 +236,7 @@ cmd_query() {
|
|
|
223
236
|
esac
|
|
224
237
|
|
|
225
238
|
local t0 out emit="cat" query_rc
|
|
226
|
-
t0=$(
|
|
239
|
+
t0=$(_now_ms)
|
|
227
240
|
out=$(_runtime "${args[@]}" 2>&1)
|
|
228
241
|
query_rc=$?
|
|
229
242
|
if [ "$query_rc" -ne 0 ]; then
|
|
@@ -235,20 +248,31 @@ cmd_query() {
|
|
|
235
248
|
} >&2
|
|
236
249
|
return "$query_rc"
|
|
237
250
|
fi
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
251
|
+
# The runtime marks a query that ran without its persistent worker (the
|
|
252
|
+
# pinned CLI started cold). Strip the marker from the evidence and carry
|
|
253
|
+
# it into the banner, the telemetry, and a one-line repair hint.
|
|
254
|
+
local cold=0 cold_json=false
|
|
255
|
+
if printf '%s\n' "$out" | grep -qx 'EGREGORE_RETRIEVAL_COLD_START'; then
|
|
256
|
+
cold=1
|
|
257
|
+
cold_json=true
|
|
258
|
+
out=$(printf '%s\n' "$out" | grep -vx 'EGREGORE_RETRIEVAL_COLD_START')
|
|
259
|
+
fi
|
|
260
|
+
# Content-free query telemetry: mode, duration, result count, degraded
|
|
261
|
+
# path. Never the query text. This is what makes Runtime/QMD retrieval
|
|
262
|
+
# visible in the telemetry event list instead of only legacy graph_query rows.
|
|
241
263
|
local _tele_count _tele_ms
|
|
242
|
-
_tele_ms=$((
|
|
264
|
+
_tele_ms=$(( $(_now_ms) - t0 ))
|
|
243
265
|
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
266
|
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
267
|
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
-
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
-
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
268
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" --argjson cold "$cold_json" \
|
|
269
|
+
'{mode:$mode, duration_ms:$d, count:$c, degraded:$cold}')" 2>/dev/null &
|
|
248
270
|
else
|
|
249
271
|
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
-
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms"
|
|
272
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson cold "$cold_json" \
|
|
273
|
+
'{mode:$mode, duration_ms:$d, degraded:$cold}')" 2>/dev/null &
|
|
251
274
|
fi
|
|
275
|
+
[ "$cold" = 1 ] && label="${label} · cold start"
|
|
252
276
|
|
|
253
277
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
254
278
|
if [ "$context_packet" = 1 ]; then
|
|
@@ -263,6 +287,9 @@ cmd_query() {
|
|
|
263
287
|
else
|
|
264
288
|
_banner_and_emit "$label" "$t0" "$emit" "$out"
|
|
265
289
|
fi
|
|
290
|
+
if [ "$cold" = 1 ]; then
|
|
291
|
+
echo " ↳ the retrieval server was not running, so this query started QMD cold. Repair: bash bin/search.sh start" >&2
|
|
292
|
+
fi
|
|
266
293
|
}
|
|
267
294
|
|
|
268
295
|
cmd_reindex() {
|
|
@@ -157,6 +157,11 @@ class QmdLocalRetriever:
|
|
|
157
157
|
self._resolved_command: tuple[str, ...] | None = None
|
|
158
158
|
self._version_checked = False
|
|
159
159
|
self._daemon_available = False
|
|
160
|
+
# Outcome of the most recent typed query: True when the owned worker
|
|
161
|
+
# answered, False when the pinned CLI ran cold, None when no query
|
|
162
|
+
# ran or persistence is disabled. Read by the harness so a slow
|
|
163
|
+
# retrieval is never silent.
|
|
164
|
+
self._last_query_used_daemon: bool | None = None
|
|
160
165
|
self._source_revision_cache: str | None = None
|
|
161
166
|
self._configure_isolated_environment()
|
|
162
167
|
self._prefer_usable_node_runtime()
|
|
@@ -726,7 +731,10 @@ class QmdLocalRetriever:
|
|
|
726
731
|
limit: int,
|
|
727
732
|
intent: str,
|
|
728
733
|
) -> list[dict[str, Any]]:
|
|
729
|
-
|
|
734
|
+
self._last_query_used_daemon = None
|
|
735
|
+
if not self._persistent_enabled():
|
|
736
|
+
daemon_error = "daemon unavailable"
|
|
737
|
+
elif self._ensure_daemon():
|
|
730
738
|
payload = {
|
|
731
739
|
"searches": searches,
|
|
732
740
|
"collections": [self.collection],
|
|
@@ -746,6 +754,7 @@ class QmdLocalRetriever:
|
|
|
746
754
|
body = json.loads(response.read().decode("utf-8"))
|
|
747
755
|
results = body.get("results") if isinstance(body, dict) else None
|
|
748
756
|
if isinstance(results, list):
|
|
757
|
+
self._last_query_used_daemon = True
|
|
749
758
|
return [item for item in results if isinstance(item, dict)]
|
|
750
759
|
except (OSError, ValueError, urllib.error.URLError) as exc:
|
|
751
760
|
self._daemon_available = False
|
|
@@ -754,8 +763,13 @@ class QmdLocalRetriever:
|
|
|
754
763
|
daemon_error = str(exc)
|
|
755
764
|
else:
|
|
756
765
|
daemon_error = "daemon returned an invalid result payload"
|
|
766
|
+
self._last_query_used_daemon = False
|
|
757
767
|
else:
|
|
768
|
+
# The owned worker could not be started or reused. The pinned CLI
|
|
769
|
+
# answers cold; the harness surfaces that instead of a silent
|
|
770
|
+
# multi-second query.
|
|
758
771
|
daemon_error = "daemon unavailable"
|
|
772
|
+
self._last_query_used_daemon = False
|
|
759
773
|
|
|
760
774
|
query_document = "\n".join(
|
|
761
775
|
f"{item['type']}: {self._single_line(item['query'])}" for item in searches
|
|
@@ -1009,6 +1023,13 @@ class QmdLocalRetriever:
|
|
|
1009
1023
|
def _persistent_enabled(self) -> bool:
|
|
1010
1024
|
return self.environment.get("EGREGORE_QMD_PERSISTENT", "1") != "0"
|
|
1011
1025
|
|
|
1026
|
+
@property
|
|
1027
|
+
def last_query_used_daemon(self) -> bool | None:
|
|
1028
|
+
"""True when the last typed query answered through the owned worker,
|
|
1029
|
+
False when it fell back to a cold CLI run, None when no query ran or
|
|
1030
|
+
the persistent worker is disabled for this instance."""
|
|
1031
|
+
return self._last_query_used_daemon
|
|
1032
|
+
|
|
1012
1033
|
def _ensure_daemon(self) -> bool:
|
|
1013
1034
|
if not self._persistent_enabled():
|
|
1014
1035
|
return False
|
|
@@ -1153,15 +1174,30 @@ class QmdLocalRetriever:
|
|
|
1153
1174
|
|
|
1154
1175
|
@staticmethod
|
|
1155
1176
|
def _port_available(port: int) -> bool:
|
|
1177
|
+
"""A port is free only when both loopback families can bind it.
|
|
1178
|
+
|
|
1179
|
+
The worker binds "localhost", which resolves to ::1 as well as
|
|
1180
|
+
127.0.0.1. Probing IPv4 alone reported a port free while a foreign
|
|
1181
|
+
server held [::1] on it; the owned worker then died on bind and every
|
|
1182
|
+
query ran the CLI cold.
|
|
1183
|
+
"""
|
|
1156
1184
|
if port == 8181:
|
|
1157
1185
|
return False
|
|
1158
|
-
|
|
1159
|
-
|
|
1186
|
+
for family, address in (
|
|
1187
|
+
(socket.AF_INET, "127.0.0.1"),
|
|
1188
|
+
(socket.AF_INET6, "::1"),
|
|
1189
|
+
):
|
|
1160
1190
|
try:
|
|
1161
|
-
probe.
|
|
1162
|
-
return True
|
|
1191
|
+
probe = socket.socket(family, socket.SOCK_STREAM)
|
|
1163
1192
|
except OSError:
|
|
1164
|
-
|
|
1193
|
+
continue # family not available on this host
|
|
1194
|
+
with probe:
|
|
1195
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
1196
|
+
try:
|
|
1197
|
+
probe.bind((address, port))
|
|
1198
|
+
except OSError:
|
|
1199
|
+
return False
|
|
1200
|
+
return True
|
|
1165
1201
|
|
|
1166
1202
|
def _allocate_port(self) -> int:
|
|
1167
1203
|
for _ in range(20):
|
|
@@ -369,6 +369,12 @@ def _query(args: argparse.Namespace) -> int:
|
|
|
369
369
|
)
|
|
370
370
|
else:
|
|
371
371
|
print("No results found.")
|
|
372
|
+
retriever = getattr(runtime, "retriever", None)
|
|
373
|
+
if not args.json and getattr(retriever, "last_query_used_daemon", None) is False:
|
|
374
|
+
# The owned QMD worker was unavailable and the pinned CLI answered
|
|
375
|
+
# cold. bin/search.sh turns this marker into a visible "cold start"
|
|
376
|
+
# note so a slow query is never silent.
|
|
377
|
+
print("EGREGORE_RETRIEVAL_COLD_START")
|
|
372
378
|
return 0
|
|
373
379
|
|
|
374
380
|
|
|
@@ -53,6 +53,40 @@ BUNDLE_TREES = ("claude", "codex", "pi", "prime")
|
|
|
53
53
|
# wins and duplicates are skipped.
|
|
54
54
|
APPLY_TREES = ("claude", "codex", "pi", "prime")
|
|
55
55
|
APPLY_TREE = APPLY_TREES[0]
|
|
56
|
+
# Bundle metadata at the root of each adapter tree; never instance content.
|
|
57
|
+
BUNDLE_MANIFEST = "manifest.json"
|
|
58
|
+
# The harness settings adapter the bundle generates; an instance's richer
|
|
59
|
+
# committed copy is preserved on activation.
|
|
60
|
+
SETTINGS_ADAPTER = ".claude/settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _settings_carries_more(existing: Path, candidate: Path) -> bool:
|
|
64
|
+
"""True when the instance's settings file has hook events or permission
|
|
65
|
+
allows the candidate's generated adapter lacks. An unreadable candidate
|
|
66
|
+
keeps the existing file; an unreadable or missing existing file does not."""
|
|
67
|
+
if not existing.is_file():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
current = json.loads(existing.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
incoming = json.loads(candidate.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
return True
|
|
77
|
+
if not isinstance(current, dict) or not isinstance(incoming, dict):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def hooks(doc: dict) -> set[str]:
|
|
81
|
+
value = doc.get("hooks")
|
|
82
|
+
return set(value.keys()) if isinstance(value, dict) else set()
|
|
83
|
+
|
|
84
|
+
def allows(doc: dict) -> set[str]:
|
|
85
|
+
perms = doc.get("permissions")
|
|
86
|
+
value = perms.get("allow") if isinstance(perms, dict) else None
|
|
87
|
+
return {str(v) for v in value} if isinstance(value, list) else set()
|
|
88
|
+
|
|
89
|
+
return bool(hooks(current) - hooks(incoming)) or bool(allows(current) - allows(incoming))
|
|
56
90
|
|
|
57
91
|
STATUS_PENDING = "pending"
|
|
58
92
|
STATUS_PREPARING = "preparing"
|
|
@@ -744,6 +778,22 @@ class UpgradeEngine:
|
|
|
744
778
|
continue
|
|
745
779
|
relative = str(source_file.relative_to(tree))
|
|
746
780
|
selected.setdefault(relative, source_file)
|
|
781
|
+
# The bundle's own manifest describes the package, not the instance;
|
|
782
|
+
# nothing in an instance reads it and it only shows up as an untracked
|
|
783
|
+
# file in the instance's repository.
|
|
784
|
+
selected.pop(BUNDLE_MANIFEST, None)
|
|
785
|
+
# The instance's own settings adapter may carry hook events or
|
|
786
|
+
# permission allows the generated public-safe adapter lacks (an org's
|
|
787
|
+
# committed .claude/settings.json). Activation never reduces it — the
|
|
788
|
+
# same rule the installer applies. A stale generated adapter (equal or
|
|
789
|
+
# a subset) still upgrades.
|
|
790
|
+
preserved: list[str] = []
|
|
791
|
+
candidate_settings = selected.get(SETTINGS_ADAPTER)
|
|
792
|
+
if candidate_settings is not None and _settings_carries_more(
|
|
793
|
+
instance / SETTINGS_ADAPTER, candidate_settings
|
|
794
|
+
):
|
|
795
|
+
selected.pop(SETTINGS_ADAPTER, None)
|
|
796
|
+
preserved.append(SETTINGS_ADAPTER)
|
|
747
797
|
# Journal opens the transaction BEFORE the first byte moves. From here
|
|
748
798
|
# until the caller clears it, a crash is recoverable from the backup.
|
|
749
799
|
self.store.write_apply_journal({
|
|
@@ -794,7 +844,12 @@ class UpgradeEngine:
|
|
|
794
844
|
(self.store.root / "rollback-files.tar.gz").unlink(missing_ok=True)
|
|
795
845
|
(self.store.root / "rollback-manifest.json").unlink(missing_ok=True)
|
|
796
846
|
raise
|
|
797
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
"applied_files": len(selected),
|
|
849
|
+
"replaced": len(replaced),
|
|
850
|
+
"added": len(added),
|
|
851
|
+
"preserved": preserved,
|
|
852
|
+
}
|
|
798
853
|
|
|
799
854
|
def activate(self) -> dict[str, Any]:
|
|
800
855
|
self._acquire_apply_lock()
|
|
@@ -886,7 +941,14 @@ class UpgradeEngine:
|
|
|
886
941
|
self.store.request_cancel()
|
|
887
942
|
worker = state.get("worker") or {}
|
|
888
943
|
pid = worker.get("pid")
|
|
889
|
-
|
|
944
|
+
status = state.get("status")
|
|
945
|
+
if status in (STATUS_PENDING, STATUS_READY, STATUS_FAILED):
|
|
946
|
+
# Nothing is running: release the staged candidate outright so a
|
|
947
|
+
# newer one can be staged. A ready or failed candidate used to be
|
|
948
|
+
# unreleasable — cancel ignored it and init refused "already staged".
|
|
949
|
+
state["status"] = STATUS_CANCELLED
|
|
950
|
+
self.store.write(state)
|
|
951
|
+
elif status == STATUS_PREPARING and not (pid and _pid_alive(pid)):
|
|
890
952
|
# No live worker will observe the flag — record the cancel now.
|
|
891
953
|
state["status"] = STATUS_CANCELLED
|
|
892
954
|
self.store.write(state)
|
|
@@ -95,19 +95,15 @@ if [ -n "$SAVED_BRANCH" ]; then
|
|
|
95
95
|
echo " ✓ Auto-saved uncommitted work on $SAVED_BRANCH"
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
-
# ---
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fi
|
|
98
|
+
# --- Org-settings sync receipts. A settings.json heal is logged to
|
|
99
|
+
# .egregore/settings-drift.log, never announced; local settings changes reach
|
|
100
|
+
# the control plane through bin/settings.sh, so there is nothing to ask. ---
|
|
102
101
|
if [ -n "${SETTINGS_SYNCED_FROM_ORG:-}" ]; then
|
|
103
102
|
echo " ◇ org settings synced from the control plane (${SETTINGS_SYNCED_FROM_ORG})"
|
|
104
103
|
fi
|
|
105
104
|
if [ -n "${SETTINGS_POSTURE_HELD:-}" ]; then
|
|
106
105
|
echo " ◇ boundary posture was set to ${SETTINGS_POSTURE_HELD} elsewhere — relaxing access needs your confirm: bash bin/settings.sh posture ${SETTINGS_POSTURE_HELD}"
|
|
107
106
|
fi
|
|
108
|
-
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
109
|
-
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
110
|
-
fi
|
|
111
107
|
|
|
112
108
|
# --- Footer: health + repos + memory (tertiary) ---
|
|
113
109
|
echo "$SEPARATOR"
|
|
@@ -61,7 +61,11 @@ settings_drift_heal() {
|
|
|
61
61
|
|
|
62
62
|
if ! git diff --quiet -- .claude/settings.json 2>/dev/null; then
|
|
63
63
|
if _settings_is_reduced_adapter; then
|
|
64
|
-
git checkout -- .claude/settings.json 2>/dev/null
|
|
64
|
+
if git checkout -- .claude/settings.json 2>/dev/null; then
|
|
65
|
+
SETTINGS_ADAPTER_RESTORED="true"
|
|
66
|
+
mkdir -p .egregore 2>/dev/null && printf '%s restored .claude/settings.json: a generated adapter had replaced the org file\n' \
|
|
67
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> .egregore/settings-drift.log 2>/dev/null || true
|
|
68
|
+
fi
|
|
65
69
|
fi
|
|
66
70
|
fi
|
|
67
71
|
|
|
@@ -32,6 +32,19 @@ _maintenance_runtime() {
|
|
|
32
32
|
python3 -m egregore_runtime.adapters.qmd_cli "$@"
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
_now_ms() {
|
|
36
|
+
if command -v perl >/dev/null 2>&1; then
|
|
37
|
+
perl -MTime::HiRes=time -e 'printf("%d\n", time() * 1000)'
|
|
38
|
+
else
|
|
39
|
+
echo $(( $(date +%s) * 1000 ))
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_elapsed_s() {
|
|
44
|
+
# One-decimal seconds since a _now_ms() timestamp.
|
|
45
|
+
awk -v ms="$(( $(_now_ms) - $1 ))" 'BEGIN { printf "%.1f", ms / 1000 }'
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
_result_paths() {
|
|
36
49
|
local line path canonical_prefix
|
|
37
50
|
canonical_prefix="**file:** \`memory/"
|
|
@@ -125,14 +138,14 @@ _banner_and_emit() {
|
|
|
125
138
|
actual="${actual%% *}"
|
|
126
139
|
required="${gap#*required=}"
|
|
127
140
|
required="${required%% *}"
|
|
128
|
-
dur=$(
|
|
141
|
+
dur=$(_elapsed_s "$t0")
|
|
129
142
|
printf '⌕ Egregore · searching your organization’s memory · %s · evidence gap · %s/%s sources · %ss\n\n' \
|
|
130
143
|
"$label" "$actual" "$required" "$dur"
|
|
131
144
|
[ "$hide_results" = 1 ] || printf '%s\n' "$out" | tail -n +2
|
|
132
145
|
return 0
|
|
133
146
|
fi
|
|
134
147
|
hits=$(printf '%s\n' "$out" | _result_paths | wc -l | tr -d ' ')
|
|
135
|
-
dur=$(
|
|
148
|
+
dur=$(_elapsed_s "$t0")
|
|
136
149
|
product="Egregore"
|
|
137
150
|
surface="searching your organization’s memory"
|
|
138
151
|
if [ "$emit" = "_enrich" ] && [ "$hits" -gt 0 ]; then
|
|
@@ -223,7 +236,7 @@ cmd_query() {
|
|
|
223
236
|
esac
|
|
224
237
|
|
|
225
238
|
local t0 out emit="cat" query_rc
|
|
226
|
-
t0=$(
|
|
239
|
+
t0=$(_now_ms)
|
|
227
240
|
out=$(_runtime "${args[@]}" 2>&1)
|
|
228
241
|
query_rc=$?
|
|
229
242
|
if [ "$query_rc" -ne 0 ]; then
|
|
@@ -235,20 +248,31 @@ cmd_query() {
|
|
|
235
248
|
} >&2
|
|
236
249
|
return "$query_rc"
|
|
237
250
|
fi
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
#
|
|
251
|
+
# The runtime marks a query that ran without its persistent worker (the
|
|
252
|
+
# pinned CLI started cold). Strip the marker from the evidence and carry
|
|
253
|
+
# it into the banner, the telemetry, and a one-line repair hint.
|
|
254
|
+
local cold=0 cold_json=false
|
|
255
|
+
if printf '%s\n' "$out" | grep -qx 'EGREGORE_RETRIEVAL_COLD_START'; then
|
|
256
|
+
cold=1
|
|
257
|
+
cold_json=true
|
|
258
|
+
out=$(printf '%s\n' "$out" | grep -vx 'EGREGORE_RETRIEVAL_COLD_START')
|
|
259
|
+
fi
|
|
260
|
+
# Content-free query telemetry: mode, duration, result count, degraded
|
|
261
|
+
# path. Never the query text. This is what makes Runtime/QMD retrieval
|
|
262
|
+
# visible in the telemetry event list instead of only legacy graph_query rows.
|
|
241
263
|
local _tele_count _tele_ms
|
|
242
|
-
_tele_ms=$((
|
|
264
|
+
_tele_ms=$(( $(_now_ms) - t0 ))
|
|
243
265
|
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
266
|
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
267
|
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
-
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
-
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
268
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" --argjson cold "$cold_json" \
|
|
269
|
+
'{mode:$mode, duration_ms:$d, count:$c, degraded:$cold}')" 2>/dev/null &
|
|
248
270
|
else
|
|
249
271
|
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
-
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms"
|
|
272
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson cold "$cold_json" \
|
|
273
|
+
'{mode:$mode, duration_ms:$d, degraded:$cold}')" 2>/dev/null &
|
|
251
274
|
fi
|
|
275
|
+
[ "$cold" = 1 ] && label="${label} · cold start"
|
|
252
276
|
|
|
253
277
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
254
278
|
if [ "$context_packet" = 1 ]; then
|
|
@@ -263,6 +287,9 @@ cmd_query() {
|
|
|
263
287
|
else
|
|
264
288
|
_banner_and_emit "$label" "$t0" "$emit" "$out"
|
|
265
289
|
fi
|
|
290
|
+
if [ "$cold" = 1 ]; then
|
|
291
|
+
echo " ↳ the retrieval server was not running, so this query started QMD cold. Repair: bash bin/search.sh start" >&2
|
|
292
|
+
fi
|
|
266
293
|
}
|
|
267
294
|
|
|
268
295
|
cmd_reindex() {
|
|
@@ -157,6 +157,11 @@ class QmdLocalRetriever:
|
|
|
157
157
|
self._resolved_command: tuple[str, ...] | None = None
|
|
158
158
|
self._version_checked = False
|
|
159
159
|
self._daemon_available = False
|
|
160
|
+
# Outcome of the most recent typed query: True when the owned worker
|
|
161
|
+
# answered, False when the pinned CLI ran cold, None when no query
|
|
162
|
+
# ran or persistence is disabled. Read by the harness so a slow
|
|
163
|
+
# retrieval is never silent.
|
|
164
|
+
self._last_query_used_daemon: bool | None = None
|
|
160
165
|
self._source_revision_cache: str | None = None
|
|
161
166
|
self._configure_isolated_environment()
|
|
162
167
|
self._prefer_usable_node_runtime()
|
|
@@ -726,7 +731,10 @@ class QmdLocalRetriever:
|
|
|
726
731
|
limit: int,
|
|
727
732
|
intent: str,
|
|
728
733
|
) -> list[dict[str, Any]]:
|
|
729
|
-
|
|
734
|
+
self._last_query_used_daemon = None
|
|
735
|
+
if not self._persistent_enabled():
|
|
736
|
+
daemon_error = "daemon unavailable"
|
|
737
|
+
elif self._ensure_daemon():
|
|
730
738
|
payload = {
|
|
731
739
|
"searches": searches,
|
|
732
740
|
"collections": [self.collection],
|
|
@@ -746,6 +754,7 @@ class QmdLocalRetriever:
|
|
|
746
754
|
body = json.loads(response.read().decode("utf-8"))
|
|
747
755
|
results = body.get("results") if isinstance(body, dict) else None
|
|
748
756
|
if isinstance(results, list):
|
|
757
|
+
self._last_query_used_daemon = True
|
|
749
758
|
return [item for item in results if isinstance(item, dict)]
|
|
750
759
|
except (OSError, ValueError, urllib.error.URLError) as exc:
|
|
751
760
|
self._daemon_available = False
|
|
@@ -754,8 +763,13 @@ class QmdLocalRetriever:
|
|
|
754
763
|
daemon_error = str(exc)
|
|
755
764
|
else:
|
|
756
765
|
daemon_error = "daemon returned an invalid result payload"
|
|
766
|
+
self._last_query_used_daemon = False
|
|
757
767
|
else:
|
|
768
|
+
# The owned worker could not be started or reused. The pinned CLI
|
|
769
|
+
# answers cold; the harness surfaces that instead of a silent
|
|
770
|
+
# multi-second query.
|
|
758
771
|
daemon_error = "daemon unavailable"
|
|
772
|
+
self._last_query_used_daemon = False
|
|
759
773
|
|
|
760
774
|
query_document = "\n".join(
|
|
761
775
|
f"{item['type']}: {self._single_line(item['query'])}" for item in searches
|
|
@@ -1009,6 +1023,13 @@ class QmdLocalRetriever:
|
|
|
1009
1023
|
def _persistent_enabled(self) -> bool:
|
|
1010
1024
|
return self.environment.get("EGREGORE_QMD_PERSISTENT", "1") != "0"
|
|
1011
1025
|
|
|
1026
|
+
@property
|
|
1027
|
+
def last_query_used_daemon(self) -> bool | None:
|
|
1028
|
+
"""True when the last typed query answered through the owned worker,
|
|
1029
|
+
False when it fell back to a cold CLI run, None when no query ran or
|
|
1030
|
+
the persistent worker is disabled for this instance."""
|
|
1031
|
+
return self._last_query_used_daemon
|
|
1032
|
+
|
|
1012
1033
|
def _ensure_daemon(self) -> bool:
|
|
1013
1034
|
if not self._persistent_enabled():
|
|
1014
1035
|
return False
|
|
@@ -1153,15 +1174,30 @@ class QmdLocalRetriever:
|
|
|
1153
1174
|
|
|
1154
1175
|
@staticmethod
|
|
1155
1176
|
def _port_available(port: int) -> bool:
|
|
1177
|
+
"""A port is free only when both loopback families can bind it.
|
|
1178
|
+
|
|
1179
|
+
The worker binds "localhost", which resolves to ::1 as well as
|
|
1180
|
+
127.0.0.1. Probing IPv4 alone reported a port free while a foreign
|
|
1181
|
+
server held [::1] on it; the owned worker then died on bind and every
|
|
1182
|
+
query ran the CLI cold.
|
|
1183
|
+
"""
|
|
1156
1184
|
if port == 8181:
|
|
1157
1185
|
return False
|
|
1158
|
-
|
|
1159
|
-
|
|
1186
|
+
for family, address in (
|
|
1187
|
+
(socket.AF_INET, "127.0.0.1"),
|
|
1188
|
+
(socket.AF_INET6, "::1"),
|
|
1189
|
+
):
|
|
1160
1190
|
try:
|
|
1161
|
-
probe.
|
|
1162
|
-
return True
|
|
1191
|
+
probe = socket.socket(family, socket.SOCK_STREAM)
|
|
1163
1192
|
except OSError:
|
|
1164
|
-
|
|
1193
|
+
continue # family not available on this host
|
|
1194
|
+
with probe:
|
|
1195
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
1196
|
+
try:
|
|
1197
|
+
probe.bind((address, port))
|
|
1198
|
+
except OSError:
|
|
1199
|
+
return False
|
|
1200
|
+
return True
|
|
1165
1201
|
|
|
1166
1202
|
def _allocate_port(self) -> int:
|
|
1167
1203
|
for _ in range(20):
|
|
@@ -369,6 +369,12 @@ def _query(args: argparse.Namespace) -> int:
|
|
|
369
369
|
)
|
|
370
370
|
else:
|
|
371
371
|
print("No results found.")
|
|
372
|
+
retriever = getattr(runtime, "retriever", None)
|
|
373
|
+
if not args.json and getattr(retriever, "last_query_used_daemon", None) is False:
|
|
374
|
+
# The owned QMD worker was unavailable and the pinned CLI answered
|
|
375
|
+
# cold. bin/search.sh turns this marker into a visible "cold start"
|
|
376
|
+
# note so a slow query is never silent.
|
|
377
|
+
print("EGREGORE_RETRIEVAL_COLD_START")
|
|
372
378
|
return 0
|
|
373
379
|
|
|
374
380
|
|