instar 1.2.54 → 1.2.56
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/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +279 -105
- package/.claude/skills/autonomous/scripts/setup-autonomous.sh +49 -2
- package/dist/core/AutonomousSessions.d.ts +66 -0
- package/dist/core/AutonomousSessions.d.ts.map +1 -0
- package/dist/core/AutonomousSessions.js +161 -0
- package/dist/core/AutonomousSessions.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +18 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +69 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +9 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +21 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +29 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +61 -61
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.2.55.md +106 -0
- package/upgrades/1.2.56.md +71 -0
- package/upgrades/side-effects/autonomous-topic-keyed-identity.md +105 -0
- package/upgrades/side-effects/multi-session-autonomy.md +102 -0
|
@@ -4,167 +4,348 @@
|
|
|
4
4
|
# Prevents session exit when autonomous mode is active.
|
|
5
5
|
# Feeds the goal and task list back to continue working.
|
|
6
6
|
#
|
|
7
|
-
# SESSION-
|
|
8
|
-
#
|
|
7
|
+
# MULTI-SESSION (per-topic state): each autonomous job has its own state file at
|
|
8
|
+
# .instar/autonomous/<topicId>.local.md, so multiple topics can run autonomous
|
|
9
|
+
# jobs concurrently without colliding. This session resolves its own topic (from
|
|
10
|
+
# its tmux session name via the topic-session registry) and reads THAT topic's
|
|
11
|
+
# state file. Ownership is therefore implicit: if my topic's file exists and is
|
|
12
|
+
# active, I am its worker. A legacy single-file job (.instar/autonomous-state.local.md)
|
|
13
|
+
# is still honored for back-compat and migrated to the per-topic path on first touch.
|
|
9
14
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# -
|
|
14
|
-
#
|
|
15
|
+
# TOPIC-KEYED OWNERSHIP survives restarts: a memory-limit restart rotates the
|
|
16
|
+
# Claude session UUID but instar respawns into the SAME tmux name, which still
|
|
17
|
+
# maps to the same topic — so the restarted session reads the same per-topic file
|
|
18
|
+
# and keeps going. (Legacy-file path keeps the v1.2.55 topic-or-liveness backstop.)
|
|
19
|
+
#
|
|
20
|
+
# RECOVERY NOTE: on a real restart-and-resume (topic file found but the recorded
|
|
21
|
+
# session UUID changed) the hook emits ONE channel-neutral recovery note + audit
|
|
22
|
+
# record, then records the new UUID so it never repeats.
|
|
23
|
+
#
|
|
24
|
+
# RESPECTS: emergency stop, duration expiry, genuine completion (promise).
|
|
15
25
|
|
|
16
|
-
set -
|
|
26
|
+
set -uo pipefail # NOTE: -e intentionally omitted; field lookups for optional
|
|
27
|
+
# frontmatter keys are expected to "fail" (grep finds nothing)
|
|
28
|
+
# and must not abort the hook. Each critical step guards itself.
|
|
17
29
|
|
|
18
30
|
# Read hook input from stdin
|
|
19
31
|
HOOK_INPUT=$(cat)
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
REGISTRY_FILE=".instar/topic-session-registry.json"
|
|
34
|
+
RECOVERY_AUDIT=".instar/autonomous-recovery.jsonl"
|
|
35
|
+
LEGACY_STATE=".instar/autonomous-state.local.md"
|
|
36
|
+
MULTI_DIR=".instar/autonomous"
|
|
37
|
+
LIVENESS_SECS="${INSTAR_AUTONOMOUS_LIVENESS_SECS:-120}"
|
|
38
|
+
|
|
39
|
+
# ── Inputs from the hook ──────────────────────────────────────────────
|
|
40
|
+
HOOK_SESSION=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
|
41
|
+
TRANSCRIPT_PATH=$(printf '%s' "$HOOK_INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
|
42
|
+
|
|
43
|
+
# If hook has no session_id → fail OPEN (unknown context, don't trap)
|
|
44
|
+
if [[ -z "$HOOK_SESSION" ]]; then
|
|
45
|
+
echo "⚠️ Autonomous mode: No session_id in hook input — fail-open (allowing exit)" >&2
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# ── Resolve MY tmux session name (the stable address) ─────────────────
|
|
50
|
+
# Test/override seam: INSTAR_HOOK_TMUX_SESSION (if the var is set at all, even
|
|
51
|
+
# empty, it wins — empty means "no tmux"). INSTAR_HOOK_NO_TMUX=1 forces empty.
|
|
52
|
+
resolve_my_tmux() {
|
|
53
|
+
if [[ "${INSTAR_HOOK_NO_TMUX:-}" == "1" ]]; then
|
|
54
|
+
echo ""
|
|
55
|
+
return
|
|
56
|
+
fi
|
|
57
|
+
if [[ -n "${INSTAR_HOOK_TMUX_SESSION+x}" ]]; then
|
|
58
|
+
echo "${INSTAR_HOOK_TMUX_SESSION}"
|
|
59
|
+
return
|
|
60
|
+
fi
|
|
61
|
+
tmux display-message -p '#S' 2>/dev/null || echo ""
|
|
62
|
+
}
|
|
63
|
+
MY_TMUX=$(resolve_my_tmux)
|
|
64
|
+
|
|
65
|
+
# Reverse-lookup: which topic does MY tmux session serve? (topicToSession is
|
|
66
|
+
# topic→tmux; we invert it to find my topic.)
|
|
67
|
+
MY_TOPIC=""
|
|
68
|
+
if [[ -n "$MY_TMUX" ]] && [[ -f "$REGISTRY_FILE" ]]; then
|
|
69
|
+
MY_TOPIC=$(INSTAR_MY_TMUX="$MY_TMUX" python3 -c "
|
|
70
|
+
import json, os
|
|
71
|
+
try:
|
|
72
|
+
reg = json.load(open('$REGISTRY_FILE'))
|
|
73
|
+
me = os.environ['INSTAR_MY_TMUX']
|
|
74
|
+
t2s = reg.get('topicToSession') or {}
|
|
75
|
+
hit = [k for k, v in t2s.items() if v == me]
|
|
76
|
+
print(hit[0] if hit else '')
|
|
77
|
+
except Exception:
|
|
78
|
+
print('')
|
|
79
|
+
" 2>/dev/null || echo "")
|
|
80
|
+
fi
|
|
23
81
|
|
|
24
|
-
|
|
25
|
-
|
|
82
|
+
# Helper: read a frontmatter field from an arbitrary state file (pipefail-safe).
|
|
83
|
+
fm_get_from() {
|
|
84
|
+
local file="$1" key="$2"
|
|
85
|
+
sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$file" 2>/dev/null | grep "^${key}:" | head -1 | sed "s/^${key}: *//" | tr -d '"' || true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ── Select the state file (per-topic preferred; legacy fallback + migrate) ──
|
|
89
|
+
STATE_FILE=""
|
|
90
|
+
OWNED_VIA_TOPIC="false"
|
|
91
|
+
PER_TOPIC_FILE=""
|
|
92
|
+
[[ -n "$MY_TOPIC" ]] && PER_TOPIC_FILE="$MULTI_DIR/${MY_TOPIC}.local.md"
|
|
93
|
+
|
|
94
|
+
if [[ -n "$PER_TOPIC_FILE" ]] && [[ -f "$PER_TOPIC_FILE" ]]; then
|
|
95
|
+
STATE_FILE="$PER_TOPIC_FILE"
|
|
96
|
+
OWNED_VIA_TOPIC="true"
|
|
97
|
+
elif [[ -f "$LEGACY_STATE" ]]; then
|
|
98
|
+
# A legacy single-file job exists. If it belongs to MY topic, migrate it to the
|
|
99
|
+
# per-topic path (idempotent, never disrupts the running job — same content).
|
|
100
|
+
LEGACY_TOPIC=$(fm_get_from "$LEGACY_STATE" report_topic)
|
|
101
|
+
if [[ -n "$MY_TOPIC" ]] && [[ "$LEGACY_TOPIC" == "$MY_TOPIC" ]]; then
|
|
102
|
+
mkdir -p "$MULTI_DIR"
|
|
103
|
+
if mv "$LEGACY_STATE" "$PER_TOPIC_FILE" 2>/dev/null; then
|
|
104
|
+
STATE_FILE="$PER_TOPIC_FILE"; OWNED_VIA_TOPIC="true"
|
|
105
|
+
echo "[autonomous] migrated legacy state → $PER_TOPIC_FILE" >&2
|
|
106
|
+
else
|
|
107
|
+
STATE_FILE="$LEGACY_STATE"
|
|
108
|
+
fi
|
|
109
|
+
else
|
|
110
|
+
# Legacy job for a different/unknown topic — honor it via the legacy
|
|
111
|
+
# ownership logic below (preserves v1.2.55 behavior for in-flight jobs).
|
|
112
|
+
STATE_FILE="$LEGACY_STATE"
|
|
113
|
+
fi
|
|
114
|
+
else
|
|
115
|
+
# No autonomous job for this session anywhere — allow exit.
|
|
26
116
|
exit 0
|
|
27
117
|
fi
|
|
28
118
|
|
|
29
|
-
#
|
|
119
|
+
# ── Read the selected state file ──────────────────────────────────────
|
|
30
120
|
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
|
|
121
|
+
fm_get() {
|
|
122
|
+
local key="$1"
|
|
123
|
+
printf '%s\n' "$FRONTMATTER" | grep "^${key}:" | head -1 | sed "s/^${key}: *//" | tr -d '"' || true
|
|
124
|
+
}
|
|
31
125
|
|
|
32
|
-
ACTIVE=$(
|
|
126
|
+
ACTIVE=$(fm_get active)
|
|
33
127
|
if [[ "$ACTIVE" != "true" ]]; then
|
|
34
128
|
exit 0
|
|
35
129
|
fi
|
|
36
130
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
131
|
+
# Paused (e.g. by quota-pressure load-shedding) — allow exit until resumed.
|
|
132
|
+
PAUSED=$(fm_get paused)
|
|
133
|
+
if [[ "$PAUSED" == "true" ]]; then
|
|
134
|
+
echo "[autonomous] job paused — allowing exit until resumed" >&2
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
41
137
|
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
138
|
+
REPORT_TOPIC=$(fm_get report_topic)
|
|
139
|
+
# Channel that owns this job — recovery note routes here. Default telegram for
|
|
140
|
+
# back-compat (state files written before channel-neutral delivery existed).
|
|
141
|
+
REPORT_CHANNEL=$(fm_get report_channel)
|
|
142
|
+
[[ -z "$REPORT_CHANNEL" ]] && REPORT_CHANNEL="telegram"
|
|
143
|
+
STATE_SESSION=$(fm_get session_id)
|
|
144
|
+
ITERATION=$(fm_get iteration)
|
|
145
|
+
DURATION_SECONDS=$(fm_get duration_seconds)
|
|
146
|
+
STARTED_AT=$(fm_get started_at)
|
|
147
|
+
COMPLETION_PROMISE=$(fm_get completion_promise)
|
|
148
|
+
|
|
149
|
+
# Validate recorded session_id is a real UUID. Claude sometimes writes a custom
|
|
150
|
+
# string instead of $CLAUDE_CODE_SESSION_ID; non-UUID values are treated as
|
|
151
|
+
# empty so the session-match backstop self-bootstraps from the real UUID.
|
|
47
152
|
UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
48
153
|
if [[ -n "$STATE_SESSION" ]] && ! [[ "$STATE_SESSION" =~ $UUID_REGEX ]]; then
|
|
49
|
-
echo "[autonomous] Invalid session_id in state file (not UUID): '$STATE_SESSION' —
|
|
154
|
+
echo "[autonomous] Invalid session_id in state file (not UUID): '$STATE_SESSION' — clearing" >&2
|
|
50
155
|
STATE_SESSION=""
|
|
51
156
|
fi
|
|
52
157
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
158
|
+
# ── Ownership decision ────────────────────────────────────────────────
|
|
159
|
+
# OWNER=true means: this session IS the autonomous worker; block its exit.
|
|
160
|
+
# OWNER_METHOD: topic (per-topic file) | topic-legacy | session | bootstrap | adopt-dead.
|
|
161
|
+
OWNER="false"
|
|
162
|
+
OWNER_METHOD=""
|
|
163
|
+
RESTART_DETECTED="false"
|
|
164
|
+
|
|
165
|
+
if [[ "$OWNED_VIA_TOPIC" == "true" ]]; then
|
|
166
|
+
# Reading MY topic's own file — ownership is implicit and unambiguous.
|
|
167
|
+
OWNER="true"; OWNER_METHOD="topic"
|
|
168
|
+
if [[ -n "$STATE_SESSION" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
169
|
+
RESTART_DETECTED="true"
|
|
170
|
+
fi
|
|
171
|
+
else
|
|
172
|
+
# ── Legacy single-file path: v1.2.55 topic-keyed-or-liveness-backstop ──
|
|
173
|
+
OWNER_TMUX=""
|
|
174
|
+
if [[ -n "$REPORT_TOPIC" ]] && [[ -f "$REGISTRY_FILE" ]]; then
|
|
175
|
+
OWNER_TMUX=$(REPORT_TOPIC="$REPORT_TOPIC" python3 -c "
|
|
176
|
+
import json, os
|
|
177
|
+
try:
|
|
178
|
+
reg = json.load(open('$REGISTRY_FILE'))
|
|
179
|
+
print((reg.get('topicToSession') or {}).get(os.environ['REPORT_TOPIC'], ''))
|
|
180
|
+
except Exception:
|
|
181
|
+
print('')
|
|
182
|
+
" 2>/dev/null || echo "")
|
|
183
|
+
fi
|
|
60
184
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
185
|
+
if [[ -n "$MY_TMUX" ]] && [[ -n "$OWNER_TMUX" ]]; then
|
|
186
|
+
if [[ "$MY_TMUX" == "$OWNER_TMUX" ]]; then
|
|
187
|
+
OWNER="true"; OWNER_METHOD="topic-legacy"
|
|
188
|
+
if [[ -n "$STATE_SESSION" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
189
|
+
RESTART_DETECTED="true"
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
exit 0 # legacy job belongs to a different session's topic
|
|
193
|
+
fi
|
|
194
|
+
else
|
|
195
|
+
# Topic unresolved — session-id backstop, liveness-gated.
|
|
196
|
+
if [[ -z "$STATE_SESSION" ]]; then
|
|
197
|
+
OWNER="true"; OWNER_METHOD="bootstrap"
|
|
198
|
+
elif [[ "$STATE_SESSION" == "$HOOK_SESSION" ]]; then
|
|
199
|
+
OWNER="true"; OWNER_METHOD="session"
|
|
200
|
+
else
|
|
201
|
+
OWNER_ALIVE="false"
|
|
202
|
+
if [[ -n "$TRANSCRIPT_PATH" ]]; then
|
|
203
|
+
OWNER_TRANSCRIPT="$(dirname "$TRANSCRIPT_PATH")/${STATE_SESSION}.jsonl"
|
|
204
|
+
if [[ -f "$OWNER_TRANSCRIPT" ]]; then
|
|
205
|
+
NOW_E=$(date +%s)
|
|
206
|
+
# GNU stat (-c %Y) first (Linux/CI); BSD stat (-f %m) fallback (macOS).
|
|
207
|
+
# GNU `stat -f` is filesystem mode and succeeds with non-numeric output,
|
|
208
|
+
# so BSD-first would mask the GNU path on Linux; numeric guard treats a
|
|
209
|
+
# bad mtime as 0 (very old → dead) rather than crashing the arithmetic.
|
|
210
|
+
MTIME=$(stat -c %Y "$OWNER_TRANSCRIPT" 2>/dev/null || stat -f %m "$OWNER_TRANSCRIPT" 2>/dev/null || echo 0)
|
|
211
|
+
[[ "$MTIME" =~ ^[0-9]+$ ]] || MTIME=0
|
|
212
|
+
AGE=$(( NOW_E - MTIME ))
|
|
213
|
+
if [[ $MTIME -gt 0 ]] && [[ $AGE -lt $LIVENESS_SECS ]]; then
|
|
214
|
+
OWNER_ALIVE="true"
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
if [[ "$OWNER_ALIVE" == "true" ]]; then
|
|
219
|
+
exit 0 # a genuinely different, live session owns this — don't steal
|
|
220
|
+
fi
|
|
221
|
+
OWNER="true"; OWNER_METHOD="adopt-dead"; RESTART_DETECTED="true"
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
70
224
|
fi
|
|
71
225
|
|
|
72
|
-
|
|
73
|
-
if [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
226
|
+
if [[ "$OWNER" != "true" ]]; then
|
|
74
227
|
exit 0
|
|
75
228
|
fi
|
|
76
229
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
|
|
80
|
-
DURATION_SECONDS=$(echo "$FRONTMATTER" | grep '^duration_seconds:' | sed 's/duration_seconds: *//')
|
|
81
|
-
STARTED_AT=$(echo "$FRONTMATTER" | grep '^started_at:' | sed 's/started_at: *//' | tr -d '"')
|
|
82
|
-
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | tr -d '"')
|
|
83
|
-
REPORT_TOPIC=$(echo "$FRONTMATTER" | grep '^report_topic:' | sed 's/report_topic: *//' | tr -d '"')
|
|
230
|
+
# ── This IS the autonomous session. Terminal checks first. ────────────
|
|
84
231
|
|
|
85
232
|
# Validate iteration
|
|
86
233
|
if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
|
|
87
234
|
echo "⚠️ Autonomous mode: State file corrupted (bad iteration)" >&2
|
|
88
|
-
rm "$STATE_FILE"
|
|
235
|
+
rm -f "$STATE_FILE"
|
|
89
236
|
exit 0
|
|
90
237
|
fi
|
|
91
238
|
|
|
92
|
-
#
|
|
239
|
+
# Duration expiry. Fail-SAFE: if started_at can't be parsed (START_EPOCH falls
|
|
240
|
+
# back to 0/empty), do NOT expire — an unparseable timestamp must never cause a
|
|
241
|
+
# premature exit (that is the very failure class this hook exists to prevent).
|
|
242
|
+
REMAINING_MIN=""
|
|
93
243
|
if [[ "$DURATION_SECONDS" =~ ^[0-9]+$ ]] && [[ $DURATION_SECONDS -gt 0 ]]; then
|
|
94
|
-
START_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
244
|
+
START_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
245
|
+
if [[ "$START_EPOCH" =~ ^[0-9]+$ ]] && [[ $START_EPOCH -gt 0 ]]; then
|
|
246
|
+
NOW_EPOCH=$(date +%s)
|
|
247
|
+
ELAPSED=$(( NOW_EPOCH - START_EPOCH ))
|
|
248
|
+
if [[ $ELAPSED -ge $DURATION_SECONDS ]]; then
|
|
249
|
+
echo "⏰ Autonomous mode: Duration expired ($ELAPSED seconds elapsed)."
|
|
250
|
+
echo " Session is free to exit."
|
|
251
|
+
rm -f "$STATE_FILE"
|
|
252
|
+
exit 0
|
|
253
|
+
fi
|
|
254
|
+
REMAINING=$(( DURATION_SECONDS - ELAPSED ))
|
|
255
|
+
REMAINING_MIN=$(( REMAINING / 60 ))
|
|
256
|
+
else
|
|
257
|
+
echo "[autonomous] started_at unparseable ('$STARTED_AT') — skipping duration-expiry check (fail-safe: keep running)" >&2
|
|
102
258
|
fi
|
|
103
|
-
REMAINING=$(( DURATION_SECONDS - ELAPSED ))
|
|
104
|
-
REMAINING_MIN=$(( REMAINING / 60 ))
|
|
105
259
|
fi
|
|
106
260
|
|
|
107
|
-
#
|
|
108
|
-
# The MessageSentinel handles this at the messaging layer, but also check here
|
|
261
|
+
# Emergency stop (global — halts every autonomous job on its next fire)
|
|
109
262
|
if [[ -f ".instar/autonomous-emergency-stop" ]]; then
|
|
110
263
|
echo "🛑 Autonomous mode: Emergency stop detected."
|
|
111
|
-
rm "$STATE_FILE"
|
|
112
|
-
|
|
264
|
+
rm -f "$STATE_FILE"
|
|
265
|
+
# NOTE: the emergency flag is left in place so OTHER topics' hooks also see it
|
|
266
|
+
# and clear their own per-topic files; stop-all clears the flag when complete.
|
|
113
267
|
exit 0
|
|
114
268
|
fi
|
|
115
269
|
|
|
116
|
-
#
|
|
117
|
-
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path' 2>/dev/null || echo "")
|
|
118
|
-
|
|
270
|
+
# Completion promise (genuine completion)
|
|
119
271
|
if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
120
|
-
# Check last assistant message for completion promise
|
|
121
272
|
LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 || echo "")
|
|
122
|
-
|
|
123
273
|
if [[ -n "$LAST_LINE" ]]; then
|
|
124
|
-
LAST_OUTPUT=$(
|
|
125
|
-
.message.content |
|
|
126
|
-
map(select(.type == "text")) |
|
|
127
|
-
map(.text) |
|
|
128
|
-
join("\n")
|
|
274
|
+
LAST_OUTPUT=$(printf '%s' "$LAST_LINE" | jq -r '
|
|
275
|
+
.message.content | map(select(.type == "text")) | map(.text) | join("\n")
|
|
129
276
|
' 2>/dev/null || echo "")
|
|
130
|
-
|
|
131
|
-
# Check for completion promise in <promise> tags
|
|
132
277
|
if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
|
|
133
|
-
PROMISE_TEXT=$(
|
|
134
|
-
|
|
278
|
+
PROMISE_TEXT=$(printf '%s' "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
|
|
135
279
|
if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
|
|
136
280
|
echo "✅ Autonomous mode: Completion promise detected — <promise>$COMPLETION_PROMISE</promise>"
|
|
137
281
|
echo " Session is free to exit. Good work!"
|
|
138
|
-
rm "$STATE_FILE"
|
|
282
|
+
rm -f "$STATE_FILE"
|
|
139
283
|
exit 0
|
|
140
284
|
fi
|
|
141
285
|
fi
|
|
142
286
|
fi
|
|
143
287
|
fi
|
|
144
288
|
|
|
145
|
-
# Not
|
|
289
|
+
# ── Not terminal: we are continuing. Handle restart-resume recovery note. ──
|
|
290
|
+
record_session_id() {
|
|
291
|
+
local new_id="$1"
|
|
292
|
+
local tmp="${STATE_FILE}.sid.$$"
|
|
293
|
+
if grep -q '^session_id:' "$STATE_FILE"; then
|
|
294
|
+
sed "s/^session_id:.*/session_id: \"${new_id}\"/" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE"
|
|
295
|
+
fi
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Channel-neutral delivery seam (no Telegram assumption). Telegram wired; other
|
|
299
|
+
# channels owned by the Channel Parity initiative. Audit record is the source of truth.
|
|
300
|
+
deliver_recovery_note() {
|
|
301
|
+
local channel="$1" target="$2" text="$3"
|
|
302
|
+
[[ -z "$target" ]] && return 0
|
|
303
|
+
case "$channel" in
|
|
304
|
+
telegram)
|
|
305
|
+
if [[ -x ".instar/scripts/telegram-reply.sh" ]]; then
|
|
306
|
+
printf '%s\n' "$text" | .instar/scripts/telegram-reply.sh "$target" >/dev/null 2>&1 || true
|
|
307
|
+
elif [[ -x ".claude/scripts/telegram-reply.sh" ]]; then
|
|
308
|
+
printf '%s\n' "$text" | .claude/scripts/telegram-reply.sh "$target" >/dev/null 2>&1 || true
|
|
309
|
+
fi
|
|
310
|
+
;;
|
|
311
|
+
*)
|
|
312
|
+
echo "[autonomous] recovery note for channel '$channel' recorded to audit; live delivery pending the Channel Parity initiative" >&2
|
|
313
|
+
;;
|
|
314
|
+
esac
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if [[ "$RESTART_DETECTED" == "true" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
318
|
+
ITER_LABEL="${ITERATION:-?}"
|
|
319
|
+
NOTE="Heads up — my session restarted mid-run and I've picked the autonomous job back up (topic ${REPORT_TOPIC:-?}, iteration ${ITER_LABEL}). No action needed."
|
|
320
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
321
|
+
printf '{"ts":"%s","event":"restart-resume","channel":"%s","topic":"%s","oldSession":"%s","newSession":"%s","method":"%s","iteration":"%s"}\n' \
|
|
322
|
+
"$TS" "$REPORT_CHANNEL" "${REPORT_TOPIC:-}" "$STATE_SESSION" "$HOOK_SESSION" "$OWNER_METHOD" "$ITER_LABEL" >> "$RECOVERY_AUDIT" 2>/dev/null || true
|
|
323
|
+
deliver_recovery_note "$REPORT_CHANNEL" "$REPORT_TOPIC" "$NOTE"
|
|
324
|
+
echo "[autonomous] restart-resume: channel=$REPORT_CHANNEL topic=${REPORT_TOPIC:-?} old=$STATE_SESSION new=$HOOK_SESSION method=$OWNER_METHOD" >&2
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Reconcile recorded session_id to live (covers restart, bootstrap, adopt).
|
|
328
|
+
if [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
|
|
329
|
+
record_session_id "$HOOK_SESSION"
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
# ── Continue the job: increment iteration, feed the task back. ────────
|
|
146
333
|
NEXT_ITERATION=$((ITERATION + 1))
|
|
147
334
|
|
|
148
|
-
# Extract prompt (everything after closing ---)
|
|
149
335
|
PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE")
|
|
150
|
-
|
|
151
336
|
if [[ -z "$PROMPT_TEXT" ]]; then
|
|
152
337
|
echo "⚠️ Autonomous mode: State file has no task content" >&2
|
|
153
|
-
rm "$STATE_FILE"
|
|
338
|
+
rm -f "$STATE_FILE"
|
|
154
339
|
exit 0
|
|
155
340
|
fi
|
|
156
341
|
|
|
157
|
-
|
|
158
|
-
TEMP_FILE
|
|
159
|
-
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE"
|
|
160
|
-
mv "$TEMP_FILE" "$STATE_FILE"
|
|
342
|
+
TEMP_FILE="${STATE_FILE}.iter.$$"
|
|
343
|
+
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE" && mv "$TEMP_FILE" "$STATE_FILE"
|
|
161
344
|
|
|
162
345
|
# ── Progress Report Check ──
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
LAST_REPORT_AT=$(echo "$FRONTMATTER" | grep '^last_report_at:' | sed 's/last_report_at: *//' | tr -d '"')
|
|
346
|
+
REPORT_INTERVAL=$(fm_get report_interval)
|
|
347
|
+
LAST_REPORT_AT=$(fm_get last_report_at)
|
|
166
348
|
|
|
167
|
-
# Convert report interval to seconds
|
|
168
349
|
REPORT_INTERVAL_SECS=1800 # default 30 minutes
|
|
169
350
|
if [[ "$REPORT_INTERVAL" =~ ^([0-9]+)m$ ]]; then
|
|
170
351
|
REPORT_INTERVAL_SECS=$(( ${BASH_REMATCH[1]} * 60 ))
|
|
@@ -174,37 +355,30 @@ fi
|
|
|
174
355
|
|
|
175
356
|
REPORT_DUE="false"
|
|
176
357
|
NOW_EPOCH=$(date +%s)
|
|
177
|
-
|
|
178
|
-
if [[ -z "$LAST_REPORT_AT" ]] || [[ "$LAST_REPORT_AT" == "null" ]] || [[ "$LAST_REPORT_AT" == '""' ]]; then
|
|
179
|
-
# No report sent yet — due if we've been running for at least one interval
|
|
358
|
+
if [[ -z "$LAST_REPORT_AT" ]] || [[ "$LAST_REPORT_AT" == "null" ]]; then
|
|
180
359
|
if [[ -n "$STARTED_AT" ]]; then
|
|
181
|
-
START_EPOCH_R=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
360
|
+
START_EPOCH_R=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
|
|
182
361
|
ELAPSED_SINCE_START=$(( NOW_EPOCH - START_EPOCH_R ))
|
|
183
362
|
if [[ $ELAPSED_SINCE_START -ge $REPORT_INTERVAL_SECS ]]; then
|
|
184
363
|
REPORT_DUE="true"
|
|
185
364
|
fi
|
|
186
365
|
fi
|
|
187
366
|
else
|
|
188
|
-
|
|
189
|
-
LAST_REPORT_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_REPORT_AT" +%s 2>/dev/null || date -d "$LAST_REPORT_AT" +%s 2>/dev/null || echo "0")
|
|
367
|
+
LAST_REPORT_EPOCH=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_REPORT_AT" +%s 2>/dev/null || date -d "$LAST_REPORT_AT" +%s 2>/dev/null || echo "0")
|
|
190
368
|
ELAPSED_SINCE_REPORT=$(( NOW_EPOCH - LAST_REPORT_EPOCH ))
|
|
191
369
|
if [[ $ELAPSED_SINCE_REPORT -ge $REPORT_INTERVAL_SECS ]]; then
|
|
192
370
|
REPORT_DUE="true"
|
|
193
371
|
fi
|
|
194
372
|
fi
|
|
195
373
|
|
|
196
|
-
# If report is due, update last_report_at in state file
|
|
197
374
|
REPORT_DIRECTIVE=""
|
|
198
375
|
if [[ "$REPORT_DUE" == "true" ]]; then
|
|
199
376
|
REPORT_NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
200
|
-
# Update or add last_report_at in frontmatter
|
|
201
377
|
if grep -q '^last_report_at:' "$STATE_FILE"; then
|
|
202
|
-
TEMP_FILE2="${STATE_FILE}.
|
|
203
|
-
sed "s/^last_report_at: .*/last_report_at: \"$REPORT_NOW\"/" "$STATE_FILE" > "$TEMP_FILE2"
|
|
204
|
-
mv "$TEMP_FILE2" "$STATE_FILE"
|
|
378
|
+
TEMP_FILE2="${STATE_FILE}.rpt.$$"
|
|
379
|
+
sed "s/^last_report_at: .*/last_report_at: \"$REPORT_NOW\"/" "$STATE_FILE" > "$TEMP_FILE2" && mv "$TEMP_FILE2" "$STATE_FILE"
|
|
205
380
|
else
|
|
206
|
-
|
|
207
|
-
TEMP_FILE2="${STATE_FILE}.tmp2.$$"
|
|
381
|
+
TEMP_FILE2="${STATE_FILE}.rpt.$$"
|
|
208
382
|
sed "0,/^---$/! { /^---$/i\\
|
|
209
383
|
last_report_at: \"$REPORT_NOW\"
|
|
210
384
|
}" "$STATE_FILE" > "$TEMP_FILE2" 2>/dev/null && mv "$TEMP_FILE2" "$STATE_FILE" || true
|
|
@@ -10,6 +10,7 @@ set -euo pipefail
|
|
|
10
10
|
GOAL=""
|
|
11
11
|
DURATION="4h"
|
|
12
12
|
REPORT_TOPIC=""
|
|
13
|
+
REPORT_CHANNEL="telegram" # channel that owns this job; recovery note routes here (telegram|slack|whatsapp|imessage)
|
|
13
14
|
LEVEL_UP="false"
|
|
14
15
|
TASKS=""
|
|
15
16
|
COMPLETION_PROMISE=""
|
|
@@ -29,6 +30,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
29
30
|
REPORT_TOPIC="$2"
|
|
30
31
|
shift 2
|
|
31
32
|
;;
|
|
33
|
+
--report-channel)
|
|
34
|
+
REPORT_CHANNEL="$2"
|
|
35
|
+
shift 2
|
|
36
|
+
;;
|
|
32
37
|
--level-up)
|
|
33
38
|
LEVEL_UP="true"
|
|
34
39
|
shift
|
|
@@ -88,10 +93,51 @@ if [[ -z "$COMPLETION_PROMISE" ]]; then
|
|
|
88
93
|
COMPLETION_PROMISE="ALL_TASKS_COMPLETE"
|
|
89
94
|
fi
|
|
90
95
|
|
|
91
|
-
#
|
|
96
|
+
# ── Multi-session start gate: concurrency cap + quota (refuse-new) ──
|
|
97
|
+
# Primary check is the server (precise active-count + QuotaTracker). If the
|
|
98
|
+
# server is unreachable, fall back to a local file-count cap so the cap still
|
|
99
|
+
# holds. Starting/restarting THIS topic's own job is always allowed.
|
|
100
|
+
if [[ -n "$REPORT_TOPIC" ]]; then
|
|
101
|
+
PORT=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
|
|
102
|
+
AUTH=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
|
|
103
|
+
CAN_START=$(curl -s -m 3 -H "Authorization: Bearer $AUTH" "http://localhost:${PORT}/autonomous/can-start?priority=medium" 2>/dev/null || echo "")
|
|
104
|
+
ALLOWED=$(printf '%s' "$CAN_START" | python3 -c "import sys,json
|
|
105
|
+
try: print(json.load(sys.stdin).get('allowed'))
|
|
106
|
+
except Exception: print('unknown')" 2>/dev/null || echo "unknown")
|
|
107
|
+
ALREADY_RUNNING="false"
|
|
108
|
+
[[ -f ".instar/autonomous/${REPORT_TOPIC}.local.md" ]] && ALREADY_RUNNING="true"
|
|
109
|
+
if [[ "$ALLOWED" == "False" ]] && [[ "$ALREADY_RUNNING" != "true" ]]; then
|
|
110
|
+
REASON=$(printf '%s' "$CAN_START" | python3 -c "import sys,json
|
|
111
|
+
try: print(json.load(sys.stdin).get('reason',''))
|
|
112
|
+
except Exception: print('')" 2>/dev/null || echo "")
|
|
113
|
+
echo "❌ Autonomous start refused: ${REASON:-cap or quota}" >&2
|
|
114
|
+
echo " Stop a running job (POST /autonomous/sessions/<topic>/stop) or raise autonomousSessions.maxConcurrent." >&2
|
|
115
|
+
exit 1
|
|
116
|
+
fi
|
|
117
|
+
if [[ "$ALLOWED" == "unknown" ]] && [[ "$ALREADY_RUNNING" != "true" ]]; then
|
|
118
|
+
MAX_CONCURRENT=$(python3 -c "import json;print((json.load(open('.instar/config.json')).get('autonomousSessions') or {}).get('maxConcurrent',5))" 2>/dev/null || echo 5)
|
|
119
|
+
COUNT=$(ls .instar/autonomous/*.local.md 2>/dev/null | grep -cv "/${REPORT_TOPIC}\.local\.md$")
|
|
120
|
+
COUNT=${COUNT:-0}
|
|
121
|
+
if [[ "$COUNT" =~ ^[0-9]+$ ]] && [[ "$MAX_CONCURRENT" =~ ^[0-9]+$ ]] && [[ $COUNT -ge $MAX_CONCURRENT ]]; then
|
|
122
|
+
echo "❌ Autonomous start refused: concurrency cap reached ($COUNT/$MAX_CONCURRENT) [server unreachable; local check]." >&2
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Create state file. Multi-session: each topic gets its own state file at
|
|
129
|
+
# .instar/autonomous/<topicId>.local.md so multiple topics run concurrent
|
|
130
|
+
# autonomous jobs without collision. With no report topic, fall back to the
|
|
131
|
+
# legacy single-file path (one-at-a-time, back-compat).
|
|
92
132
|
mkdir -p .instar
|
|
133
|
+
if [[ -n "$REPORT_TOPIC" ]]; then
|
|
134
|
+
mkdir -p .instar/autonomous
|
|
135
|
+
STATE_PATH=".instar/autonomous/${REPORT_TOPIC}.local.md"
|
|
136
|
+
else
|
|
137
|
+
STATE_PATH=".instar/autonomous-state.local.md"
|
|
138
|
+
fi
|
|
93
139
|
|
|
94
|
-
cat >
|
|
140
|
+
cat > "$STATE_PATH" <<EOF
|
|
95
141
|
---
|
|
96
142
|
active: true
|
|
97
143
|
iteration: 1
|
|
@@ -102,6 +148,7 @@ duration_seconds: $DURATION_SECONDS
|
|
|
102
148
|
started_at: "$STARTED_AT"
|
|
103
149
|
end_at: "$END_AT"
|
|
104
150
|
report_topic: "$REPORT_TOPIC"
|
|
151
|
+
report_channel: "$REPORT_CHANNEL"
|
|
105
152
|
report_interval: "$REPORT_INTERVAL"
|
|
106
153
|
last_report_at: ""
|
|
107
154
|
level_up: $LEVEL_UP
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutonomousSessions — multi-session autonomy control surface.
|
|
3
|
+
*
|
|
4
|
+
* Each autonomous job has its own state file at
|
|
5
|
+
* `<stateDir>/autonomous/<topicId>.local.md`. A legacy single-file job at
|
|
6
|
+
* `<stateDir>/autonomous-state.local.md` is also recognized for back-compat.
|
|
7
|
+
*
|
|
8
|
+
* This module is the read/control layer over those files: list active jobs,
|
|
9
|
+
* enforce the concurrency cap + quota at start, and stop jobs (all or one).
|
|
10
|
+
* The stop hook (`autonomous-stop-hook.sh`) is the per-session enforcer; this
|
|
11
|
+
* module is what the server routes, the start path, and the stop-everything
|
|
12
|
+
* path call. It never traps a session itself — it only reports and clears state.
|
|
13
|
+
*/
|
|
14
|
+
import type { JobPriority } from './types.js';
|
|
15
|
+
/** Default concurrent autonomous-job cap when config doesn't specify one. */
|
|
16
|
+
export declare const DEFAULT_MAX_CONCURRENT_AUTONOMOUS = 5;
|
|
17
|
+
export interface AutonomousJobSummary {
|
|
18
|
+
topic: string | null;
|
|
19
|
+
file: string;
|
|
20
|
+
active: boolean;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
goal: string | null;
|
|
23
|
+
iteration: number | null;
|
|
24
|
+
startedAt: string | null;
|
|
25
|
+
reportChannel: string | null;
|
|
26
|
+
}
|
|
27
|
+
/** All autonomous jobs (per-topic files + a legacy single file if present). */
|
|
28
|
+
export declare function listAutonomousJobs(stateDir: string): AutonomousJobSummary[];
|
|
29
|
+
/** Active (and not paused) autonomous jobs — what counts against the cap. */
|
|
30
|
+
export declare function activeAutonomousJobs(stateDir: string): AutonomousJobSummary[];
|
|
31
|
+
export interface CanStartDeps {
|
|
32
|
+
stateDir: string;
|
|
33
|
+
maxConcurrent: number;
|
|
34
|
+
quotaCanStart?: (priority?: JobPriority) => {
|
|
35
|
+
allowed: boolean;
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
38
|
+
priority?: JobPriority;
|
|
39
|
+
}
|
|
40
|
+
export interface CanStartResult {
|
|
41
|
+
allowed: boolean;
|
|
42
|
+
reason: string;
|
|
43
|
+
activeCount: number;
|
|
44
|
+
maxConcurrent: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether a new autonomous job may start: concurrency cap first
|
|
48
|
+
* (refuse-new at the cap), then quota (refuse-new under budget pressure).
|
|
49
|
+
* Never preempts a running job — that's the pause path, handled elsewhere.
|
|
50
|
+
*/
|
|
51
|
+
export declare function canStartAutonomousJob(deps: CanStartDeps): CanStartResult;
|
|
52
|
+
/** Stop exactly one topic's job (removes its state file). Returns true if removed. */
|
|
53
|
+
export declare function stopAutonomousTopic(stateDir: string, topic: string): boolean;
|
|
54
|
+
export interface StopAllResult {
|
|
55
|
+
stoppedTopics: string[];
|
|
56
|
+
stoppedLegacy: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Stop every autonomous job. Removes all per-topic files + the legacy file and
|
|
60
|
+
* writes the emergency-stop flag so any session whose hook fires before its file
|
|
61
|
+
* is gone also stands down. The flag is the belt; removing files is the suspenders.
|
|
62
|
+
*/
|
|
63
|
+
export declare function stopAllAutonomousJobs(stateDir: string): StopAllResult;
|
|
64
|
+
/** Pause one topic's job (hook will allow exit until resumed). Returns true if updated. */
|
|
65
|
+
export declare function pauseAutonomousTopic(stateDir: string, topic: string): boolean;
|
|
66
|
+
//# sourceMappingURL=AutonomousSessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AutonomousSessions.d.ts","sourceRoot":"","sources":["../../src/core/AutonomousSessions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,6EAA6E;AAC7E,eAAO,MAAM,iCAAiC,IAAI,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAmCD,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAmB3E;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAE7E;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,KAAK;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,YAAY,GAAG,cAAc,CAmBxE;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO5E;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAwBrE;AAED,2FAA2F;AAC3F,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAW7E"}
|