@adaptic/maestro 1.6.1 → 1.7.0
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 +1 -1
- package/scripts/continuous-monitor.sh +11 -6
- package/scripts/daemon/context-compiler.mjs +8 -8
- package/scripts/daemon/health.mjs +2 -2
- package/scripts/daemon/maestro-daemon.mjs +4 -3
- package/scripts/email_thread_dedup.py +4 -3
- package/scripts/huddle/huddle-server.mjs +50 -29
- package/scripts/llm_email_dedup.py +23 -15
- package/scripts/local-triggers/generate-plists.sh +2 -1
- package/scripts/media-generation/README.md +1 -1
- package/scripts/outbound-dedup-cleanup.sh +4 -4
- package/scripts/outbound-dedup.sh +4 -3
- package/scripts/pdf-generation/README.md +1 -1
- package/scripts/pdf-generation/templates/memo.latex +1 -1
- package/scripts/poll-slack-events.sh +4 -2
- package/scripts/poller/imap-client.mjs +11 -10
- package/scripts/poller/index.mjs +6 -6
- package/scripts/poller/intra-session-check.mjs +35 -18
- package/scripts/poller/mehran-gmail-poller.mjs +63 -29
- package/scripts/poller/slack-poller.mjs +45 -31
- package/scripts/poller/trigger.mjs +22 -5
- package/scripts/pre-draft-context.py +2 -2
- package/scripts/rag-indexer.py +3 -3
- package/scripts/send-sms.sh +7 -7
- package/scripts/send-whatsapp.sh +11 -11
- package/scripts/setup/configure-macos.sh +4 -2
- package/scripts/setup/init-agent.sh +1 -1
- package/scripts/slack-react.mjs +1 -1
- package/scripts/slack-typing.mjs +3 -3
- package/scripts/system-verify.sh +28 -15
- package/scripts/user-context-search.py +4 -4
- package/scripts/validate-outbound.py +29 -18
- package/scripts/sophie-inbox-poller.py +0 -406
package/scripts/poller/index.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Agent Poller — Lightweight event detection daemon
|
|
3
3
|
// Runs every 60 seconds via macOS launchd
|
|
4
4
|
// Polls Slack, Gmail, Calendar for new items
|
|
5
|
-
// Writes to state/inbox/ for
|
|
5
|
+
// Writes to state/inbox/ for the agent daemon to process
|
|
6
6
|
|
|
7
7
|
import { pollSlack } from "./slack-poller.mjs";
|
|
8
8
|
import { pollGmail } from "./gmail-poller.mjs";
|
|
9
9
|
import { pollCalendar } from "./calendar-poller.mjs";
|
|
10
10
|
import { pollMehranGmail } from "./mehran-gmail-poller.mjs";
|
|
11
|
-
import {
|
|
12
|
-
import { isPriorityItem,
|
|
11
|
+
import { triggerAgent } from "./trigger.mjs";
|
|
12
|
+
import { isPriorityItem, AGENT_REPO_DIR } from "./utils.mjs";
|
|
13
13
|
import { appendFileSync, mkdirSync } from "fs";
|
|
14
14
|
import { join } from "path";
|
|
15
15
|
|
|
16
16
|
async function main() {
|
|
17
17
|
const timestamp = new Date().toISOString();
|
|
18
|
-
const logDir = join(
|
|
18
|
+
const logDir = join(AGENT_REPO_DIR, "logs", "polling");
|
|
19
19
|
mkdirSync(logDir, { recursive: true });
|
|
20
20
|
const logFile = join(logDir, `${timestamp.split("T")[0]}-poller.jsonl`);
|
|
21
21
|
|
|
@@ -38,7 +38,7 @@ async function main() {
|
|
|
38
38
|
|
|
39
39
|
for (const item of result.items) {
|
|
40
40
|
if (isPriorityItem(item) && !priorityTriggered) {
|
|
41
|
-
|
|
41
|
+
triggerAgent(item);
|
|
42
42
|
priorityTriggered = true;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -7,22 +7,36 @@
|
|
|
7
7
|
// 3. Unprocessed inbox items
|
|
8
8
|
// 4. Writing priority triggers when CEO messages detected
|
|
9
9
|
//
|
|
10
|
-
// This complements the 60s launchd poller — it ensures
|
|
11
|
-
// messages while deep in tasks within a session.
|
|
10
|
+
// This complements the 60s launchd poller — it ensures the agent doesn't
|
|
11
|
+
// miss messages while deep in tasks within a session.
|
|
12
12
|
|
|
13
|
-
import { readdirSync, writeFileSync, mkdirSync, appendFileSync } from "fs";
|
|
13
|
+
import { readdirSync, writeFileSync, mkdirSync, appendFileSync, readFileSync } from "fs";
|
|
14
14
|
import { join, dirname } from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const
|
|
18
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
|
|
19
19
|
|
|
20
20
|
const SLACK_TOKEN = process.env.SLACK_USER_TOKEN || process.env.SLACK_TOKEN;
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// Identity loaded from canonical SOT so principal/agent user IDs aren't
|
|
23
|
+
// hardcoded. The CEO_DM_CHANNEL is detected dynamically (channels starting
|
|
24
|
+
// with "D" are IMs); the previous hardcoded value worked only for Sophie.
|
|
25
|
+
function loadAgentIdentity() {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return { slackMemberId: "", firstName: "Agent", principal: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const _agent = loadAgentIdentity();
|
|
33
|
+
const AGENT_USER_ID = _agent.slackMemberId || "";
|
|
34
|
+
const CEO_USER_ID = _agent.principal?.slackMemberId || "";
|
|
35
|
+
// CEO_DM_CHANNEL is agent-specific (Slack assigns IM channel IDs dynamically).
|
|
36
|
+
// Provide it via env var, otherwise the CEO-DM hot path is skipped on this
|
|
37
|
+
// poll cycle and we rely on the normal channel scan + Slack Events API to
|
|
38
|
+
// catch principal messages.
|
|
39
|
+
const CEO_DM_CHANNEL = process.env.CEO_DM_CHANNEL || "";
|
|
26
40
|
|
|
27
41
|
// Critical channels to check during intra-session polls
|
|
28
42
|
const CRITICAL_CHANNELS = [
|
|
@@ -31,11 +45,11 @@ const CRITICAL_CHANNELS = [
|
|
|
31
45
|
{ id: "C097N5R8HUJ", name: "all-adaptic" },
|
|
32
46
|
];
|
|
33
47
|
|
|
34
|
-
const TRIGGER_DIR = join(
|
|
48
|
+
const TRIGGER_DIR = join(AGENT_REPO_DIR, "state", "triggers", "priority");
|
|
35
49
|
const INBOX_DIRS = ["slack", "gmail", "calendar", "sms", "internal"].map((s) =>
|
|
36
|
-
join(
|
|
50
|
+
join(AGENT_REPO_DIR, "state", "inbox", s),
|
|
37
51
|
);
|
|
38
|
-
const LOG_DIR = join(
|
|
52
|
+
const LOG_DIR = join(AGENT_REPO_DIR, "logs", "polling");
|
|
39
53
|
|
|
40
54
|
async function slackApi(method, params = {}) {
|
|
41
55
|
const url = new URL(`https://slack.com/api/${method}`);
|
|
@@ -76,6 +90,7 @@ content: |
|
|
|
76
90
|
|
|
77
91
|
async function checkCeoDm() {
|
|
78
92
|
if (!SLACK_TOKEN) return { found: false, error: "SLACK_TOKEN not set" };
|
|
93
|
+
if (!CEO_DM_CHANNEL) return { found: false, error: "CEO_DM_CHANNEL not configured" };
|
|
79
94
|
|
|
80
95
|
// Check last 5 minutes of CEO DM channel
|
|
81
96
|
const oldest = String((Date.now() - 5 * 60 * 1000) / 1000);
|
|
@@ -96,10 +111,11 @@ async function checkCeoDm() {
|
|
|
96
111
|
|
|
97
112
|
if (ceoMessages.length > 0) {
|
|
98
113
|
const latest = ceoMessages[0];
|
|
114
|
+
const principalSlug = (_agent.principal?.fullName || "principal").toLowerCase().replace(/\s+/g, "-");
|
|
99
115
|
const triggerFile = writePriorityTrigger(
|
|
100
116
|
`slack:${CEO_DM_CHANNEL}:${latest.ts}`,
|
|
101
|
-
|
|
102
|
-
`dm
|
|
117
|
+
principalSlug,
|
|
118
|
+
`dm/${principalSlug}`,
|
|
103
119
|
latest.text,
|
|
104
120
|
"CEO DM detected during intra-session poll",
|
|
105
121
|
);
|
|
@@ -134,9 +150,10 @@ async function checkCriticalChannels() {
|
|
|
134
150
|
if (!result.ok) continue;
|
|
135
151
|
|
|
136
152
|
const relevant = (result.messages || []).filter(
|
|
137
|
-
(m) => !m.bot_id && m.user !==
|
|
153
|
+
(m) => !m.bot_id && m.user !== AGENT_USER_ID,
|
|
138
154
|
);
|
|
139
155
|
|
|
156
|
+
const agentNameLower = (_agent.firstName || "").toLowerCase();
|
|
140
157
|
if (relevant.length > 0) {
|
|
141
158
|
newMessages.push({
|
|
142
159
|
channel: ch.name,
|
|
@@ -145,9 +162,9 @@ async function checkCriticalChannels() {
|
|
|
145
162
|
hasUrgent: relevant.some((m) =>
|
|
146
163
|
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(m.text || ""),
|
|
147
164
|
),
|
|
148
|
-
|
|
149
|
-
(m.text || "").toLowerCase().includes(
|
|
150
|
-
|
|
165
|
+
hasAgentMention: agentNameLower
|
|
166
|
+
? relevant.some((m) => (m.text || "").toLowerCase().includes(agentNameLower))
|
|
167
|
+
: false,
|
|
151
168
|
});
|
|
152
169
|
|
|
153
170
|
// If CEO posted in a critical channel, also create a trigger
|
|
@@ -231,7 +248,7 @@ async function main() {
|
|
|
231
248
|
const flags = [];
|
|
232
249
|
if (c.hasCeo) flags.push("CEO");
|
|
233
250
|
if (c.hasUrgent) flags.push("URGENT");
|
|
234
|
-
if (c.
|
|
251
|
+
if (c.hasAgentMention) flags.push(`@${_agent.firstName || "Agent"}`);
|
|
235
252
|
const flagStr = flags.length > 0 ? ` [${flags.join(",")}]` : "";
|
|
236
253
|
return `#${c.channel}(${c.count})${flagStr}`;
|
|
237
254
|
})
|
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Uses the shared imap-client.mjs module
|
|
1
|
+
// Principal Gmail Poller — polls the principal's inbox via native Node.js IMAP.
|
|
2
|
+
// Uses the shared imap-client.mjs module.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// ACCESS BOUNDARY: This poller is only enabled for agents whose archetype
|
|
5
|
+
// explicitly grants principal-Gmail access (typically Chief-of-Staff /
|
|
6
|
+
// executive-operator archetypes). For other agents, leave
|
|
7
|
+
// PRINCIPAL_GMAIL_APP_PASSWORD unset and this module will no-op.
|
|
8
|
+
//
|
|
9
|
+
// Backward compatibility: this file was previously named `mehran-gmail-poller.mjs`
|
|
10
|
+
// with `pollMehranGmail` as the exported function. The new export name is
|
|
11
|
+
// `pollPrincipalGmail`; the old name is re-exported as an alias.
|
|
5
12
|
|
|
6
13
|
import { readFileSync, readdirSync } from "fs";
|
|
7
14
|
import { join } from "path";
|
|
8
|
-
import {
|
|
15
|
+
import { AGENT_REPO_DIR, writeCursor } from "./utils.mjs";
|
|
9
16
|
import { pollImapInbox } from "./imap-client.mjs";
|
|
10
17
|
|
|
11
|
-
const INBOX_DIR = join(
|
|
18
|
+
const INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "gmail");
|
|
19
|
+
|
|
20
|
+
// Load principal identity from agent.json — email comes from there, password
|
|
21
|
+
// from env (PRINCIPAL_GMAIL_APP_PASSWORD with MEHRAN_GMAIL_APP_PASSWORD fallback
|
|
22
|
+
// for repos still on the legacy var name).
|
|
23
|
+
function loadPrincipal() {
|
|
24
|
+
try {
|
|
25
|
+
const agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
26
|
+
return agent.principal || {};
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
function loadMehranPassword() {
|
|
32
|
+
function loadPrincipalPassword() {
|
|
33
|
+
if (process.env.PRINCIPAL_GMAIL_APP_PASSWORD) return process.env.PRINCIPAL_GMAIL_APP_PASSWORD;
|
|
17
34
|
if (process.env.MEHRAN_GMAIL_APP_PASSWORD) return process.env.MEHRAN_GMAIL_APP_PASSWORD;
|
|
18
35
|
try {
|
|
19
|
-
const envFile = readFileSync(join(
|
|
36
|
+
const envFile = readFileSync(join(AGENT_REPO_DIR, ".env"), "utf-8");
|
|
20
37
|
for (const line of envFile.split("\n")) {
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
const m = line.match(/^(PRINCIPAL_GMAIL_APP_PASSWORD|MEHRAN_GMAIL_APP_PASSWORD)=(.*)$/);
|
|
39
|
+
if (m) {
|
|
40
|
+
let val = m[2].trim();
|
|
23
41
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
24
42
|
val = val.slice(1, -1);
|
|
25
43
|
}
|
|
@@ -30,33 +48,46 @@ function loadMehranPassword() {
|
|
|
30
48
|
return "";
|
|
31
49
|
}
|
|
32
50
|
|
|
33
|
-
export async function
|
|
51
|
+
export async function pollPrincipalGmail() {
|
|
34
52
|
const items = [];
|
|
35
53
|
const errors = [];
|
|
36
54
|
|
|
37
|
-
|
|
55
|
+
const principal = loadPrincipal();
|
|
56
|
+
const principalEmail = principal.email;
|
|
57
|
+
const password = loadPrincipalPassword();
|
|
58
|
+
|
|
59
|
+
// No-op if not configured — agents without principal-Gmail access just
|
|
60
|
+
// skip this entirely instead of failing every poll cycle.
|
|
61
|
+
if (!principalEmail || !password) {
|
|
62
|
+
return { items, errors };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const accountSlug = (principal.firstName || "principal").toLowerCase();
|
|
66
|
+
const fileSuffix = `${accountSlug}-email`;
|
|
67
|
+
|
|
38
68
|
try {
|
|
39
69
|
const result = await pollImapInbox({
|
|
40
|
-
email:
|
|
41
|
-
password
|
|
42
|
-
account:
|
|
43
|
-
fileSuffix
|
|
44
|
-
eventType:
|
|
45
|
-
logPrefix:
|
|
70
|
+
email: principalEmail,
|
|
71
|
+
password,
|
|
72
|
+
account: accountSlug,
|
|
73
|
+
fileSuffix,
|
|
74
|
+
eventType: `${accountSlug}_email`,
|
|
75
|
+
logPrefix: `[${accountSlug}-gmail]`,
|
|
46
76
|
});
|
|
47
77
|
if (result.errors.length > 0) {
|
|
48
78
|
errors.push(...result.errors);
|
|
49
79
|
}
|
|
50
80
|
} catch (err) {
|
|
51
|
-
const msg = `
|
|
52
|
-
console.error(`[
|
|
81
|
+
const msg = `Principal Gmail poll failed: ${err.message}`;
|
|
82
|
+
console.error(`[${accountSlug}-gmail] ${msg}`);
|
|
53
83
|
errors.push(msg);
|
|
54
84
|
}
|
|
55
85
|
|
|
56
86
|
// Read resulting JSON files
|
|
57
87
|
try {
|
|
88
|
+
const fileGlob = `-${fileSuffix}.json`;
|
|
58
89
|
const files = readdirSync(INBOX_DIR).filter(
|
|
59
|
-
(f) => f.endsWith(
|
|
90
|
+
(f) => f.endsWith(fileGlob) && !f.endsWith(".processed"),
|
|
60
91
|
);
|
|
61
92
|
|
|
62
93
|
for (const file of files) {
|
|
@@ -65,9 +96,9 @@ export async function pollMehranGmail() {
|
|
|
65
96
|
const record = JSON.parse(content);
|
|
66
97
|
|
|
67
98
|
items.push({
|
|
68
|
-
id: file.replace(
|
|
69
|
-
service:
|
|
70
|
-
channel:
|
|
99
|
+
id: file.replace(fileGlob, ""),
|
|
100
|
+
service: `${accountSlug}-gmail`,
|
|
101
|
+
channel: `${accountSlug}-inbox`,
|
|
71
102
|
sender: record.email?.from || "unknown",
|
|
72
103
|
sender_privilege: "unknown",
|
|
73
104
|
timestamp: record.received_at || new Date().toISOString(),
|
|
@@ -79,9 +110,9 @@ export async function pollMehranGmail() {
|
|
|
79
110
|
from_ceo: false,
|
|
80
111
|
tagged_urgent: false,
|
|
81
112
|
contains_deadline: false,
|
|
82
|
-
|
|
113
|
+
mentions_agent: false,
|
|
83
114
|
},
|
|
84
|
-
raw_ref:
|
|
115
|
+
raw_ref: `${accountSlug}-gmail:${record.email?.message_id || ""}`,
|
|
85
116
|
});
|
|
86
117
|
} catch (parseErr) {
|
|
87
118
|
errors.push(`Failed to parse ${file}: ${parseErr.message}`);
|
|
@@ -93,6 +124,9 @@ export async function pollMehranGmail() {
|
|
|
93
124
|
}
|
|
94
125
|
}
|
|
95
126
|
|
|
96
|
-
writeCursor(
|
|
127
|
+
writeCursor(`${accountSlug}-gmail`, new Date().toISOString());
|
|
97
128
|
return { items, errors };
|
|
98
129
|
}
|
|
130
|
+
|
|
131
|
+
// Back-compat: old export name preserved so existing imports continue to work.
|
|
132
|
+
export const pollMehranGmail = pollPrincipalGmail;
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
writeCursor,
|
|
10
10
|
resolvePrivilege,
|
|
11
11
|
resolveName,
|
|
12
|
-
|
|
12
|
+
AGENT_REPO_DIR,
|
|
13
13
|
extractSlackAttachments,
|
|
14
14
|
downloadSlackAttachment,
|
|
15
15
|
} from "./utils.mjs";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "../parse-voice-transcript.mjs";
|
|
22
22
|
|
|
23
23
|
const ACTIVE_THREADS_FILE = join(
|
|
24
|
-
|
|
24
|
+
AGENT_REPO_DIR,
|
|
25
25
|
"state",
|
|
26
26
|
"slack-active-threads.json",
|
|
27
27
|
);
|
|
@@ -29,6 +29,18 @@ const MAX_THREADS_PER_CHANNEL = 10;
|
|
|
29
29
|
|
|
30
30
|
const SLACK_TOKEN = process.env.SLACK_USER_TOKEN || process.env.SLACK_TOKEN;
|
|
31
31
|
|
|
32
|
+
// Load agent identity from canonical SOT for self-filtering + display names.
|
|
33
|
+
let _agent = null;
|
|
34
|
+
function loadAgent() {
|
|
35
|
+
if (_agent) return _agent;
|
|
36
|
+
try {
|
|
37
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
38
|
+
} catch {
|
|
39
|
+
_agent = { firstName: "Agent", fullName: "Agent", slackMemberId: "" };
|
|
40
|
+
}
|
|
41
|
+
return _agent;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
// Priority overrides for known channels — everything else defaults to "normal"
|
|
33
45
|
const CHANNEL_PRIORITY_OVERRIDES = {
|
|
34
46
|
"ceo-office": "critical",
|
|
@@ -46,7 +58,7 @@ let channelsCachedAt = 0;
|
|
|
46
58
|
const CHANNEL_CACHE_TTL = 10 * 60 * 1000; // 10 min
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
|
-
* Fetch all channels
|
|
61
|
+
* Fetch all channels the agent is a member of.
|
|
50
62
|
* Replaces the static MONITORED_CHANNELS list so new channels are auto-discovered.
|
|
51
63
|
* Per CEO standing instruction si-001: "Monitor ALL Slack channels."
|
|
52
64
|
*/
|
|
@@ -80,7 +92,7 @@ async function getMonitoredChannels() {
|
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
const CEO_USER_ID = "U097N5R0M7U";
|
|
83
|
-
const
|
|
95
|
+
const AGENT_USER_ID = "U099N1JFPRQ";
|
|
84
96
|
|
|
85
97
|
// Rate-limit-aware delay with random jitter to reduce Slack API rate limiting.
|
|
86
98
|
// Base delay 400ms + random 0-600ms jitter = 400-1000ms between requests.
|
|
@@ -165,7 +177,7 @@ const CONTEXT_MESSAGES_LIMIT = 8; // Last 8 messages from the conversation
|
|
|
165
177
|
|
|
166
178
|
/**
|
|
167
179
|
* Fetch recent conversation context for a DM message.
|
|
168
|
-
* Returns the last N messages (including
|
|
180
|
+
* Returns the last N messages (including the agent's) formatted as readable text.
|
|
169
181
|
* This gives the session awareness of the conversation flow.
|
|
170
182
|
*/
|
|
171
183
|
async function fetchDMContext(channelId, beforeTs) {
|
|
@@ -181,7 +193,7 @@ async function fetchDMContext(channelId, beforeTs) {
|
|
|
181
193
|
// Messages come newest-first from Slack, reverse for chronological order
|
|
182
194
|
const msgs = result.messages.reverse();
|
|
183
195
|
const lines = msgs.map((m) => {
|
|
184
|
-
const who = m.user ===
|
|
196
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
185
197
|
const text = (m.text || "").substring(0, 500);
|
|
186
198
|
return `${who}: ${text}`;
|
|
187
199
|
});
|
|
@@ -211,7 +223,7 @@ async function fetchThreadContext(channelId, threadTs, beforeTs) {
|
|
|
211
223
|
if (preceding.length === 0) return null;
|
|
212
224
|
|
|
213
225
|
const lines = preceding.map((m) => {
|
|
214
|
-
const who = m.user ===
|
|
226
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
215
227
|
const text = (m.text || "").substring(0, 500);
|
|
216
228
|
return `${who}: ${text}`;
|
|
217
229
|
});
|
|
@@ -290,12 +302,12 @@ export async function pollSlack() {
|
|
|
290
302
|
const channelThreads = new Set(activeThreads[channel.id] || []);
|
|
291
303
|
|
|
292
304
|
for (const msg of result.messages || []) {
|
|
293
|
-
// Register any message with replies as an active thread (even
|
|
305
|
+
// Register any message with replies as an active thread (even the agent's)
|
|
294
306
|
if (msg.reply_count > 0 && msg.ts) {
|
|
295
307
|
channelThreads.add(msg.ts);
|
|
296
308
|
}
|
|
297
309
|
|
|
298
|
-
if (msg.bot_id || msg.user ===
|
|
310
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
299
311
|
|
|
300
312
|
const msgText = msg.text || "";
|
|
301
313
|
const isCeo = msg.user === CEO_USER_ID;
|
|
@@ -313,7 +325,8 @@ export async function pollSlack() {
|
|
|
313
325
|
continue;
|
|
314
326
|
}
|
|
315
327
|
|
|
316
|
-
const
|
|
328
|
+
const agentNameLower = (loadAgent().firstName || "").toLowerCase();
|
|
329
|
+
const mentionsAgent = !!agentNameLower && msgText.toLowerCase().includes(agentNameLower);
|
|
317
330
|
const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
|
|
318
331
|
msgText,
|
|
319
332
|
);
|
|
@@ -350,7 +363,7 @@ export async function pollSlack() {
|
|
|
350
363
|
from_ceo: isCeo,
|
|
351
364
|
tagged_urgent: isUrgent,
|
|
352
365
|
contains_deadline: false,
|
|
353
|
-
|
|
366
|
+
mentions_agent: mentionsAgent,
|
|
354
367
|
},
|
|
355
368
|
raw_ref: `slack:${channel.id}:${msg.ts}`,
|
|
356
369
|
});
|
|
@@ -409,7 +422,7 @@ export async function pollSlack() {
|
|
|
409
422
|
channelThreads.add(msg.ts);
|
|
410
423
|
}
|
|
411
424
|
|
|
412
|
-
if (msg.bot_id || msg.user ===
|
|
425
|
+
if (msg.bot_id || msg.user === AGENT_USER_ID) continue;
|
|
413
426
|
|
|
414
427
|
const msgText = msg.text || "";
|
|
415
428
|
const isCeo = msg.user === CEO_USER_ID;
|
|
@@ -465,7 +478,7 @@ export async function pollSlack() {
|
|
|
465
478
|
tagged_urgent:
|
|
466
479
|
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(msgText),
|
|
467
480
|
contains_deadline: false,
|
|
468
|
-
|
|
481
|
+
mentions_agent: true,
|
|
469
482
|
},
|
|
470
483
|
raw_ref: `slack:${im.id}:${msg.ts}`,
|
|
471
484
|
});
|
|
@@ -500,7 +513,7 @@ export async function pollSlack() {
|
|
|
500
513
|
// previous poll cycles; without this check the daemon re-classifies and
|
|
501
514
|
// re-dispatches the same thread reply every 60 seconds until the reply
|
|
502
515
|
// ages past the lookback window (the original "thread scanning bug").
|
|
503
|
-
const SLACK_INBOX = join(
|
|
516
|
+
const SLACK_INBOX = join(AGENT_REPO_DIR, "state", "inbox", "slack");
|
|
504
517
|
let existingInboxFiles = [];
|
|
505
518
|
try {
|
|
506
519
|
existingInboxFiles = readdirSync(SLACK_INBOX);
|
|
@@ -551,34 +564,34 @@ export async function pollSlack() {
|
|
|
551
564
|
const replies = repliesResult.messages || [];
|
|
552
565
|
|
|
553
566
|
// ── Thread-level cooldown ──────────────────────────────────
|
|
554
|
-
// If
|
|
567
|
+
// If the agent posted ANY reply in this thread within the last
|
|
555
568
|
// THREAD_COOLDOWN_MS, skip the entire thread. This prevents
|
|
556
569
|
// re-ingesting human messages that a running session is already
|
|
557
570
|
// handling, which previously caused cascading holding notes.
|
|
558
571
|
const THREAD_COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes
|
|
559
572
|
const cooldownTs = (Date.now() - THREAD_COOLDOWN_MS) / 1000;
|
|
560
|
-
const
|
|
573
|
+
const agentRepliedRecently = replies.some(
|
|
561
574
|
(r) =>
|
|
562
|
-
r.user ===
|
|
575
|
+
r.user === AGENT_USER_ID &&
|
|
563
576
|
parseFloat(r.ts) > cooldownTs,
|
|
564
577
|
);
|
|
565
|
-
if (
|
|
578
|
+
if (agentRepliedRecently) continue; // skip entire thread
|
|
566
579
|
|
|
567
580
|
for (const reply of replies) {
|
|
568
581
|
// Skip the parent message itself (ts === thread_ts)
|
|
569
582
|
if (reply.ts === threadTs) continue;
|
|
570
|
-
// Skip bot messages and
|
|
571
|
-
if (reply.bot_id || reply.user ===
|
|
583
|
+
// Skip bot messages and the agent's own messages
|
|
584
|
+
if (reply.bot_id || reply.user === AGENT_USER_ID) continue;
|
|
572
585
|
// Only process replies within our lookback window
|
|
573
586
|
if (parseFloat(reply.ts) <= threadOldestTs) continue;
|
|
574
587
|
|
|
575
|
-
// Skip messages that
|
|
576
|
-
// If
|
|
588
|
+
// Skip messages that the agent already replied to in this thread.
|
|
589
|
+
// If the agent posted a message AFTER this human message, a prior
|
|
577
590
|
// session already handled it — don't re-ingest and duplicate.
|
|
578
|
-
const
|
|
579
|
-
(r) => r.user ===
|
|
591
|
+
const agentRepliedAfter = replies.some(
|
|
592
|
+
(r) => r.user === AGENT_USER_ID && parseFloat(r.ts) > parseFloat(reply.ts)
|
|
580
593
|
);
|
|
581
|
-
if (
|
|
594
|
+
if (agentRepliedAfter) continue;
|
|
582
595
|
|
|
583
596
|
const replyText = reply.text || "";
|
|
584
597
|
const isCeo = reply.user === CEO_USER_ID;
|
|
@@ -604,7 +617,7 @@ export async function pollSlack() {
|
|
|
604
617
|
const precedingInThread = replies
|
|
605
618
|
.filter((m) => parseFloat(m.ts) < parseFloat(reply.ts))
|
|
606
619
|
.map((m) => {
|
|
607
|
-
const who = m.user ===
|
|
620
|
+
const who = m.user === AGENT_USER_ID ? (loadAgent().firstName || "Agent") : resolveName(m.user);
|
|
608
621
|
return `${who}: ${(m.text || "").substring(0, 500)}`;
|
|
609
622
|
})
|
|
610
623
|
.join("\n");
|
|
@@ -649,7 +662,7 @@ export async function pollSlack() {
|
|
|
649
662
|
tagged_urgent:
|
|
650
663
|
/\b(urgent|emergency|asap|blocker|critical)\b/i.test(replyText),
|
|
651
664
|
contains_deadline: false,
|
|
652
|
-
|
|
665
|
+
mentions_agent: isDmThread || (loadAgent().firstName && new RegExp(loadAgent().firstName, "i").test(replyText)),
|
|
653
666
|
},
|
|
654
667
|
raw_ref: `slack:${channelId}:${reply.ts}`,
|
|
655
668
|
});
|
|
@@ -675,7 +688,7 @@ export async function pollSlack() {
|
|
|
675
688
|
// These include @mentions, channel thread replies, and reactions that
|
|
676
689
|
// the conversations.history API doesn't return. Scan for unprocessed
|
|
677
690
|
// .json files and convert them into items for the daemon pipeline.
|
|
678
|
-
const SLACK_INBOX_DIR = join(
|
|
691
|
+
const SLACK_INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "slack");
|
|
679
692
|
try {
|
|
680
693
|
const inboxFiles = readdirSync(SLACK_INBOX_DIR)
|
|
681
694
|
.filter((f) => f.endsWith(".json") && !f.endsWith(".processed"))
|
|
@@ -704,10 +717,11 @@ export async function pollSlack() {
|
|
|
704
717
|
const evt = data.slack_event || data.event || {};
|
|
705
718
|
const sender = data.sender || resolveName(data.user || data.user_id || evt.user || "unknown");
|
|
706
719
|
|
|
707
|
-
// Skip
|
|
720
|
+
// Skip the agent's own messages — the events server should filter these,
|
|
708
721
|
// but if any slip through (e.g. message_changed edge cases), catch here
|
|
709
722
|
const userId = data.user || data.user_id || evt.user || "";
|
|
710
|
-
|
|
723
|
+
const myFullSlug = (loadAgent().fullName || "").toLowerCase().replace(/\s+/g, "-");
|
|
724
|
+
if (userId === AGENT_USER_ID || (myFullSlug && sender === myFullSlug)) {
|
|
711
725
|
try { renameSync(join(SLACK_INBOX_DIR, file), join(SLACK_INBOX_DIR, file + ".processed")); } catch {}
|
|
712
726
|
continue;
|
|
713
727
|
}
|
|
@@ -750,7 +764,7 @@ export async function pollSlack() {
|
|
|
750
764
|
from_ceo: privilege === "ceo",
|
|
751
765
|
tagged_urgent: /\b(urgent|emergency|asap|blocker|critical)\b/i.test(content),
|
|
752
766
|
contains_deadline: false,
|
|
753
|
-
|
|
767
|
+
mentions_agent: loadAgent().firstName ? new RegExp(loadAgent().firstName, "i").test(content) : false,
|
|
754
768
|
},
|
|
755
769
|
raw_ref: eventRef,
|
|
756
770
|
_inbox_file: file,
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
// Priority Trigger — writes a priority task file when urgent items detected
|
|
2
|
-
import { writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { AGENT_REPO_DIR } from "./utils.mjs";
|
|
5
5
|
|
|
6
6
|
const COOLDOWN_MS = 30_000;
|
|
7
7
|
let lastTrigger = 0;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Cached agent identity — used to label the "mentions agent" reason
|
|
10
|
+
// without hardcoding any one agent's name in the reason string.
|
|
11
|
+
let _agent = null;
|
|
12
|
+
function loadAgent() {
|
|
13
|
+
if (_agent) return _agent;
|
|
14
|
+
try {
|
|
15
|
+
_agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
_agent = { firstName: "Agent" };
|
|
18
|
+
}
|
|
19
|
+
return _agent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Back-compat export: callers used to invoke `triggerSophie`. The function
|
|
23
|
+
// is fully agent-neutral now; both names map to the same implementation.
|
|
24
|
+
export function triggerAgent(item) {
|
|
10
25
|
const now = Date.now();
|
|
11
26
|
if (now - lastTrigger < COOLDOWN_MS) {
|
|
12
27
|
console.log(
|
|
@@ -19,13 +34,13 @@ export function triggerSophie(item) {
|
|
|
19
34
|
const reasons = [];
|
|
20
35
|
if (item.priority_signals?.from_ceo) reasons.push("Message from CEO");
|
|
21
36
|
if (item.priority_signals?.tagged_urgent) reasons.push("Tagged urgent");
|
|
22
|
-
if (item.priority_signals?.
|
|
37
|
+
if (item.priority_signals?.mentions_agent) reasons.push(`Mentions ${loadAgent().firstName}`);
|
|
23
38
|
const reason = reasons.join(", ") || "Priority item";
|
|
24
39
|
|
|
25
40
|
console.log(`[trigger] Priority event detected: ${reason}`);
|
|
26
41
|
|
|
27
42
|
try {
|
|
28
|
-
const dir = join(
|
|
43
|
+
const dir = join(AGENT_REPO_DIR, "state", "inbox", "internal");
|
|
29
44
|
mkdirSync(dir, { recursive: true });
|
|
30
45
|
const taskFile = join(dir, `priority-${Date.now()}.yaml`);
|
|
31
46
|
let content = `type: priority_trigger
|
|
@@ -56,3 +71,5 @@ content: |
|
|
|
56
71
|
return false;
|
|
57
72
|
}
|
|
58
73
|
}
|
|
74
|
+
|
|
75
|
+
export const triggerSophie = triggerAgent;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""Pre-draft fact retrieval module — Memory Enhancement Phase 4.
|
|
3
3
|
|
|
4
4
|
Pulls relevant entity context BEFORE composing any outbound message.
|
|
5
|
-
Closes the gap where
|
|
6
|
-
|
|
5
|
+
Closes the gap where the agent drafts messages without first checking what
|
|
6
|
+
they know about the recipient and related entities.
|
|
7
7
|
|
|
8
8
|
Integration points — call this script before any of these send scripts:
|
|
9
9
|
- scripts/send-email-threaded.py (email composition)
|
package/scripts/rag-indexer.py
CHANGED
|
@@ -27,7 +27,7 @@ Usage:
|
|
|
27
27
|
python3 scripts/rag-indexer.py --stats # show index statistics
|
|
28
28
|
python3 scripts/rag-indexer.py --db /path/to/db # custom DB path
|
|
29
29
|
|
|
30
|
-
Author:
|
|
30
|
+
Author: Adaptic.ai
|
|
31
31
|
Date: 2026-04-03
|
|
32
32
|
"""
|
|
33
33
|
|
|
@@ -43,7 +43,7 @@ from pathlib import Path
|
|
|
43
43
|
|
|
44
44
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
-
BASE_DIR = Path(__file__).resolve().parent.parent #
|
|
46
|
+
BASE_DIR = Path(os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))) # agent repo root
|
|
47
47
|
DEFAULT_DB_PATH = BASE_DIR / "state" / "rag" / "search.db"
|
|
48
48
|
INDEX_STATE_PATH = BASE_DIR / "state" / "rag" / "index-state.json"
|
|
49
49
|
|
|
@@ -590,7 +590,7 @@ def show_stats(db_path):
|
|
|
590
590
|
def main():
|
|
591
591
|
parser = argparse.ArgumentParser(
|
|
592
592
|
description="SQLite FTS5 indexer for per-user ring-fenced RAG (Phase 2). "
|
|
593
|
-
"Indexes
|
|
593
|
+
"Indexes the agent repo's content sources into a full-text search database.",
|
|
594
594
|
epilog="Examples:\n"
|
|
595
595
|
" %(prog)s # incremental index\n"
|
|
596
596
|
" %(prog)s --full # full re-index\n"
|