@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 = [...ceoDMs, ...rotatedOtherDMs];
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"
@@ -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