@adaptic/maestro 1.10.6 → 1.10.8
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/package.json
CHANGED
|
@@ -273,6 +273,59 @@ export async function pollSlack() {
|
|
|
273
273
|
activeThreads = {};
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
// ── CEO DM pre-pass ──────────────────────────────────────────────────
|
|
277
|
+
// Mehran's DMs are the single most time-sensitive signal. The channel
|
|
278
|
+
// loop below routinely eats the entire 55s cycle budget on a busy
|
|
279
|
+
// workspace and skips DMs entirely. Poll the CEO DMs FIRST (with a
|
|
280
|
+
// small dedicated budget) so they always get fresh data regardless of
|
|
281
|
+
// how the broader cycle plays out. The downstream DM section will
|
|
282
|
+
// re-iterate but the items dedup on raw_ref so duplicates are dropped.
|
|
283
|
+
const ceoDmItemsSeen = new Set(); // raw_refs we already pushed in the pre-pass
|
|
284
|
+
try {
|
|
285
|
+
const convos = await slackApi("conversations.list", { types: "im", limit: 50 });
|
|
286
|
+
if (convos.ok) {
|
|
287
|
+
const ceoDMs = (convos.channels || []).filter((im) => im.user === CEO_USER_ID);
|
|
288
|
+
for (const im of ceoDMs) {
|
|
289
|
+
const history = await slackApi("conversations.history", {
|
|
290
|
+
channel: im.id,
|
|
291
|
+
oldest,
|
|
292
|
+
limit: 10,
|
|
293
|
+
});
|
|
294
|
+
if (!history.ok) continue;
|
|
295
|
+
for (const msg of history.messages || []) {
|
|
296
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
297
|
+
if (isVoiceTranscript(msg.text || "")) continue; // handled in main DM loop
|
|
298
|
+
const rawRef = `slack:${im.id}:${msg.ts}`;
|
|
299
|
+
ceoDmItemsSeen.add(rawRef);
|
|
300
|
+
items.push({
|
|
301
|
+
id: msg.ts.replace(".", "-"),
|
|
302
|
+
service: "slack",
|
|
303
|
+
channel: `dm/${resolveName(msg.user)}`,
|
|
304
|
+
sender: resolveName(msg.user),
|
|
305
|
+
sender_privilege: resolvePrivilege(msg.user),
|
|
306
|
+
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
|
|
307
|
+
subject: `DM from ${resolveName(msg.user)}`,
|
|
308
|
+
content: msg.text || "",
|
|
309
|
+
thread_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : "",
|
|
310
|
+
is_reply: false,
|
|
311
|
+
is_voice_transcript: false,
|
|
312
|
+
priority_signals: {
|
|
313
|
+
from_ceo: true,
|
|
314
|
+
tagged_urgent:
|
|
315
|
+
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(msg.text || ""),
|
|
316
|
+
contains_deadline: false,
|
|
317
|
+
mentions_agent: true,
|
|
318
|
+
},
|
|
319
|
+
raw_ref: rawRef,
|
|
320
|
+
channel_id: im.id,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
errors.push(`CEO DM pre-pass: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
276
329
|
for (const channel of channelsThisCycle) {
|
|
277
330
|
// Check cycle time budget — skip remaining channels if running long
|
|
278
331
|
if (isCycleBudgetExceeded()) {
|
|
@@ -391,12 +444,16 @@ export async function pollSlack() {
|
|
|
391
444
|
});
|
|
392
445
|
if (convos.ok) {
|
|
393
446
|
const allDMs = convos.channels || [];
|
|
394
|
-
// Separate CEO DMs (always poll) from other DMs (rotate in halves)
|
|
447
|
+
// Separate CEO DMs (always poll) from other DMs (rotate in halves).
|
|
448
|
+
// We still track ceoDMChannelIds for the downstream thread-scan
|
|
449
|
+
// priority pass, but skip CEO DMs in the per-channel polling loop
|
|
450
|
+
// below because the CEO DM pre-pass at the top of the cycle has
|
|
451
|
+
// already fetched + pushed their messages with fresh budget.
|
|
395
452
|
const ceoDMs = allDMs.filter((im) => im.user === CEO_USER_ID);
|
|
396
453
|
for (const dm of ceoDMs) ceoDMChannelIds.add(dm.id);
|
|
397
454
|
const otherDMs = allDMs.filter((im) => im.user !== CEO_USER_ID);
|
|
398
455
|
const rotatedOtherDMs = otherDMs.filter((_, i) => i % 2 === pollCycleCount % 2);
|
|
399
|
-
const dmsThisCycle =
|
|
456
|
+
const dmsThisCycle = rotatedOtherDMs;
|
|
400
457
|
|
|
401
458
|
for (const im of dmsThisCycle) {
|
|
402
459
|
// Check cycle time budget — skip remaining DMs if running long
|
|
@@ -159,6 +159,49 @@ EOF
|
|
|
159
159
|
exit 1 # Not yet handled
|
|
160
160
|
;;
|
|
161
161
|
|
|
162
|
+
release)
|
|
163
|
+
# release <channel_id> <message_ts> [session_id]
|
|
164
|
+
# Releases an unconfirmed lock so a legitimate retry isn't blocked by a
|
|
165
|
+
# 24h TTL. Called by slack-send.sh's exit trap when a send fails or the
|
|
166
|
+
# script crashes after acquiring the lock but before confirming the send.
|
|
167
|
+
#
|
|
168
|
+
# Safety:
|
|
169
|
+
# - Refuse to release if the lock has been confirmed (real send happened)
|
|
170
|
+
# - If session_id is provided, only release locks owned by that session
|
|
171
|
+
# (prevents a buggy caller from blowing away a sibling's in-flight lock)
|
|
172
|
+
#
|
|
173
|
+
# Lock metadata is stored in `meta` (acquire/confirm both write to it)
|
|
174
|
+
# using the format produced by the acquire/confirm sections above:
|
|
175
|
+
# session: "<id>"
|
|
176
|
+
# status: acquired (or confirmed)
|
|
177
|
+
CHANNEL="${2:?Channel ID required}"
|
|
178
|
+
MSG_TS="${3:?Message TS required}"
|
|
179
|
+
SESSION_ID="${4:-unknown}"
|
|
180
|
+
|
|
181
|
+
LOCK_DIR="$LOCK_BASE_DIR/${CHANNEL}-${MSG_TS}"
|
|
182
|
+
META="$LOCK_DIR/meta"
|
|
183
|
+
|
|
184
|
+
if [ ! -d "$LOCK_DIR" ]; then
|
|
185
|
+
echo "NOT_HELD"
|
|
186
|
+
exit 0
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
if [ -f "$META" ] && grep -qE "^status:[[:space:]]*confirmed" "$META" 2>/dev/null; then
|
|
190
|
+
echo "ALREADY_CONFIRMED"
|
|
191
|
+
exit 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
if [ -f "$META" ] && [ "$SESSION_ID" != "unknown" ]; then
|
|
195
|
+
if ! grep -qE "^session:[[:space:]]*\"?${SESSION_ID}\"?[[:space:]]*$" "$META" 2>/dev/null; then
|
|
196
|
+
echo "NOT_OWNED"
|
|
197
|
+
exit 0
|
|
198
|
+
fi
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
rm -rf "$LOCK_DIR"
|
|
202
|
+
echo "RELEASED"
|
|
203
|
+
;;
|
|
204
|
+
|
|
162
205
|
mark)
|
|
163
206
|
# mark <message_ts> — backward-compatible mark
|
|
164
207
|
MSG_TS="${2:?Message TS required}"
|
|
@@ -175,11 +218,12 @@ EOF
|
|
|
175
218
|
;;
|
|
176
219
|
|
|
177
220
|
*)
|
|
178
|
-
echo "Usage: slack-responded.sh {acquire|confirm|check|mark|clean} [args...]"
|
|
221
|
+
echo "Usage: slack-responded.sh {acquire|confirm|release|check|mark|clean} [args...]"
|
|
179
222
|
echo ""
|
|
180
223
|
echo "Commands:"
|
|
181
224
|
echo " acquire <channel> <msg_ts> [session_id] — Atomically claim a response lock"
|
|
182
225
|
echo " confirm <channel> <msg_ts> [preview] — Record successful send"
|
|
226
|
+
echo " release <channel> <msg_ts> [session_id] — Release unconfirmed lock (send failed)"
|
|
183
227
|
echo " check <msg_ts> [channel] — Check if already handled"
|
|
184
228
|
echo " mark <msg_ts> — Legacy: mark as handled"
|
|
185
229
|
echo " clean — Purge old entries"
|
package/scripts/slack-send.sh
CHANGED
|
@@ -72,6 +72,36 @@ if [ -z "$CHANNEL" ] || [ -z "$MESSAGE" ]; then
|
|
|
72
72
|
exit 1
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# Auto-detect --responding_to from the inbox if the caller didn't pass it.
|
|
76
|
+
# Protects against operator-vs-daemon duplicate replies: when a human-prompted
|
|
77
|
+
# session replies via this script, it should claim the dedup lock so the
|
|
78
|
+
# daemon's session (which always passes --responding_to) finds the lock
|
|
79
|
+
# already taken and skips its own send. Without auto-detect, two reply
|
|
80
|
+
# sources race and the user gets duplicate substantive responses (observed
|
|
81
|
+
# on 2026-05-12 when operator + daemon both replied to a CEO DM 3 min apart).
|
|
82
|
+
#
|
|
83
|
+
# Strategy: scan state/inbox/slack/ for the most recent unprocessed YAML
|
|
84
|
+
# whose body references this channel; extract its raw_ref ts as RESPONDING_TO.
|
|
85
|
+
# Operator can override by passing --responding_to explicitly, or skip auto-
|
|
86
|
+
# detect with --responding_to "" (proactive send not tied to an inbox item).
|
|
87
|
+
if [ -z "$RESPONDING_TO" ] && [[ "$CHANNEL" =~ ^[CD][A-Z0-9]+$ ]]; then
|
|
88
|
+
INBOX_DIR="$AGENT_REPO_DIR/state/inbox/slack"
|
|
89
|
+
if [ -d "$INBOX_DIR" ]; then
|
|
90
|
+
LATEST_INBOX=$(
|
|
91
|
+
ls -t "$INBOX_DIR"/*.yaml 2>/dev/null |
|
|
92
|
+
xargs -I{} grep -l "channel_id: \"$CHANNEL\"" {} 2>/dev/null |
|
|
93
|
+
head -1
|
|
94
|
+
)
|
|
95
|
+
if [ -n "$LATEST_INBOX" ]; then
|
|
96
|
+
AUTO_TS=$(grep "^raw_ref:" "$LATEST_INBOX" 2>/dev/null | sed -E 's/.*:([0-9]+\.[0-9]+)".*/\1/')
|
|
97
|
+
if [ -n "$AUTO_TS" ]; then
|
|
98
|
+
RESPONDING_TO="$AUTO_TS"
|
|
99
|
+
echo "[slack-send] auto-detected --responding_to=$RESPONDING_TO from $(basename "$LATEST_INBOX")" >&2
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
75
105
|
# Dedup check: if --responding_to is set, use atomic locking to prevent concurrent sends
|
|
76
106
|
DEDUP_ACQUIRED=""
|
|
77
107
|
if [ -n "$RESPONDING_TO" ]; then
|
|
@@ -88,6 +118,21 @@ if [ -n "$RESPONDING_TO" ]; then
|
|
|
88
118
|
exit 0
|
|
89
119
|
fi
|
|
90
120
|
DEDUP_ACQUIRED="1"
|
|
121
|
+
|
|
122
|
+
# Lock-release safety net. If this script exits BEFORE confirming the
|
|
123
|
+
# lock (e.g. validation failed, Slack API errored, python script crashed
|
|
124
|
+
# during message conversion, caller killed us), we must release the lock
|
|
125
|
+
# so the next legitimate retry isn't blocked for 24 hours. The trap is
|
|
126
|
+
# cleared after the post-send confirm; if the confirm runs, we leave
|
|
127
|
+
# the lock in place (it's now a real send marker, not a placeholder).
|
|
128
|
+
cleanup_unconfirmed_lock() {
|
|
129
|
+
local exit_code=$?
|
|
130
|
+
if [ -n "$DEDUP_ACQUIRED" ] && [ -z "$DEDUP_CONFIRMED" ]; then
|
|
131
|
+
"$SCRIPT_DIR/slack-responded.sh" release "$CHANNEL" "$RESPONDING_TO" "$SESSION_ID" 2>/dev/null || true
|
|
132
|
+
fi
|
|
133
|
+
return "$exit_code"
|
|
134
|
+
}
|
|
135
|
+
trap cleanup_unconfirmed_lock EXIT
|
|
91
136
|
fi
|
|
92
137
|
|
|
93
138
|
# Show typing indicator before sending (non-blocking, fire-and-forget)
|
|
@@ -200,7 +245,11 @@ if [ "$STATUS" = "invalid_auth" ] || [ "$STATUS" = "token_revoked" ] || [ "$STAT
|
|
|
200
245
|
fi
|
|
201
246
|
|
|
202
247
|
# Post-send: confirm the response lock with message details (audit trail)
|
|
248
|
+
# and mark the trap as fulfilled so the exit-handler does NOT release the
|
|
249
|
+
# now-legitimate confirmation marker. Any non-OK status leaves DEDUP_CONFIRMED
|
|
250
|
+
# unset, so the EXIT trap releases the lock for legitimate retry.
|
|
203
251
|
if [ "$STATUS" = "OK" ] && [ -n "$RESPONDING_TO" ] && [ -n "$DEDUP_ACQUIRED" ]; then
|
|
204
252
|
PREVIEW=$(echo "$MESSAGE" | head -c 200)
|
|
205
253
|
"$SCRIPT_DIR/slack-responded.sh" confirm "$CHANNEL" "$RESPONDING_TO" "$PREVIEW" 2>/dev/null || true
|
|
254
|
+
DEDUP_CONFIRMED="1"
|
|
206
255
|
fi
|