@indigoai-us/hq-cli 5.345.197 → 5.345.199
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/CHANGELOG.md +111 -0
- package/dist/command-catalog.generated.d.ts +393 -12
- package/dist/command-catalog.generated.js +482 -12
- package/dist/commands/bot-events.d.ts +19 -0
- package/dist/commands/bot-events.js +46 -0
- package/dist/commands/bot-jobs.d.ts +2 -0
- package/dist/commands/bot-jobs.js +43 -2
- package/dist/commands/bot.d.ts +5 -0
- package/dist/commands/bot.js +33 -1
- package/dist/commands/core-resolve-company-native.d.ts +2 -1
- package/dist/commands/core-resolve-company-native.js +2 -24
- package/dist/commands/deploy-domains.d.ts +14 -2
- package/dist/commands/deploy-domains.js +726 -42
- package/dist/commands/integrations-connect.js +41 -3
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +44 -1
- package/dist/commands/lanes.d.ts +9 -0
- package/dist/commands/lanes.js +95 -9
- package/dist/commands/meetings.js +1 -21
- package/dist/commands/secrets.js +27 -6
- package/dist/commands/signals-run.d.ts +2 -0
- package/dist/commands/signals-run.js +6 -1
- package/dist/lanes-drain-hook.js +9 -0
- package/dist/lib/bot/api.d.ts +5 -0
- package/dist/lib/bot/claude-login.d.ts +31 -0
- package/dist/lib/bot/claude-login.js +160 -0
- package/dist/lib/bot/company-bind.d.ts +34 -0
- package/dist/lib/bot/company-bind.js +112 -0
- package/dist/lib/bot/config.js +3 -1
- package/dist/lib/bot/events.d.ts +18 -0
- package/dist/lib/bot/events.js +160 -0
- package/dist/lib/bot/executive-settings.d.ts +10 -0
- package/dist/lib/bot/executive-settings.js +43 -0
- package/dist/lib/bot/fleet-cron.js +4 -0
- package/dist/lib/bot/fleet-local.d.ts +12 -0
- package/dist/lib/bot/fleet-local.js +50 -20
- package/dist/lib/bot/job-run.js +1 -1
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
- package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
- package/dist/lib/bot/jobs/meeting-detect.js +176 -0
- package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
- package/dist/lib/bot/jobs/meeting-rider.js +284 -0
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
- package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
- package/dist/lib/bot/local-jobs.d.ts +15 -2
- package/dist/lib/bot/local-jobs.js +24 -7
- package/dist/lib/bot/run.d.ts +7 -49
- package/dist/lib/bot/run.js +135 -12
- package/dist/lib/bot/session.d.ts +19 -1
- package/dist/lib/bot/session.js +75 -28
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/core-utils/manifest-slugs.d.ts +2 -0
- package/dist/lib/core-utils/manifest-slugs.js +26 -0
- package/dist/lib/doctor/checks/bots.d.ts +6 -0
- package/dist/lib/doctor/checks/bots.js +47 -1
- package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
- package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
- package/dist/lib/hot-channels/schedule.js +2 -2
- package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
- package/dist/lib/hot-channels/signals-pass.js +51 -17
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +4 -0
- package/dist/lib/lanes/approval-gate.js +4 -1
- package/dist/lib/lanes/links.d.ts +8 -1
- package/dist/lib/lanes/links.js +84 -3
- package/dist/lib/lanes/outcome.d.ts +154 -0
- package/dist/lib/lanes/outcome.js +151 -0
- package/dist/lib/lanes/reconcile.js +1 -0
- package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
- package/dist/lib/lanes/signal-brief-template.js +40 -0
- package/dist/lib/lanes/signal-lane.d.ts +24 -0
- package/dist/lib/lanes/signal-lane.js +99 -0
- package/dist/lib/lanes/source-report.d.ts +37 -0
- package/dist/lib/lanes/source-report.js +66 -0
- package/dist/lib/lanes/spawn.d.ts +6 -0
- package/dist/lib/lanes/spawn.js +21 -0
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +3 -0
- package/dist/lib/lanes/story-ledger.js +1 -0
- package/dist/lib/lanes/types.d.ts +137 -0
- package/dist/lib/lanes/types.js +17 -2
- package/dist/lib/lanes/watcher.js +4 -0
- package/dist/lib/live-meetings.d.ts +19 -0
- package/dist/lib/live-meetings.js +23 -0
- package/dist/lib/mesh/live/daemon/doctor.js +6 -7
- package/dist/lib/mesh/live/spool.d.ts +8 -0
- package/dist/lib/mesh/live/spool.js +59 -40
- package/dist/lib/plan-limit-nag.d.ts +30 -4
- package/dist/lib/plan-limit-nag.js +68 -17
- package/dist/lib/signals/engine.d.ts +4 -0
- package/dist/lib/signals/engine.js +25 -5
- package/dist/lib/signals/incremental.d.ts +42 -0
- package/dist/lib/signals/incremental.js +191 -0
- package/dist/lib/signals/prior-signals.d.ts +11 -0
- package/dist/lib/signals/prior-signals.js +44 -0
- package/dist/lib/signals/reply.js +6 -1
- package/dist/lib/signals/settings.d.ts +5 -0
- package/dist/lib/signals/settings.js +17 -0
- package/dist/lib/signals/steps/extract.d.ts +30 -2
- package/dist/lib/signals/steps/extract.js +153 -11
- package/dist/lib/signals/steps/types.d.ts +1 -0
- package/dist/lib/signals/write-guard.js +7 -0
- package/dist/secrets-env-action.d.ts +70 -0
- package/dist/secrets-env-action.js +220 -2
- package/dist/utils/cognito-callback-port.d.ts +15 -0
- package/dist/utils/cognito-callback-port.js +51 -0
- package/dist/utils/cognito-config.js +2 -3
- package/dist/utils/cognito-session.d.ts +7 -6
- package/dist/utils/cognito-session.js +14 -14
- package/dist/utils/interactive-caller.d.ts +15 -0
- package/dist/utils/interactive-caller.js +70 -0
- package/dist/utils/login-browser.js +3 -1
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +9 -7
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/** The meeting-specific producer; extraction and event consumers remain source-independent. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
import { ensureCognitoToken } from "../../../utils/cognito-session.js";
|
|
9
|
+
import { getCompanyUid, vaultApiFetch } from "../../../utils/vault-api.js";
|
|
10
|
+
import { assertSafeSegment } from "../../hot-channels/landing.js";
|
|
11
|
+
import { meetingSignalSourceRef, cursorPath } from "../../signals/incremental.js";
|
|
12
|
+
import { readAllLanes } from "../../lanes/store.js";
|
|
13
|
+
import { runSignals } from "../../signals/engine.js";
|
|
14
|
+
import { emitBotEvent, EVENT_PAYLOAD_BYTES } from "../events.js";
|
|
15
|
+
import { executiveRiderEnabled } from "../executive-settings.js";
|
|
16
|
+
export { isExecutiveBot } from "../executive-settings.js";
|
|
17
|
+
import { readLocalJobsStrict, withJobsLock, writeLocalJobs } from "../local-jobs.js";
|
|
18
|
+
import { readBuiltinJobsMarker, writeBuiltinJobsMarker } from "./hot-signal-sync.js";
|
|
19
|
+
export const MEETING_RIDER_JOB_KIND = "meeting-rider";
|
|
20
|
+
export const END_POLLS = 3;
|
|
21
|
+
const MAX_LOCK_AGE = 6 * 60 * 60 * 1000;
|
|
22
|
+
const TERMINAL = new Set(["completed", "done", "no_transcript", "failed", "fatal", "error", "cancelled", "canceled", "ended", "call_ended", "left", "processing"]);
|
|
23
|
+
const terminal = (status) => TERMINAL.has(status.toLowerCase().replaceAll("-", "_"));
|
|
24
|
+
function atomic(file, value) {
|
|
25
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
26
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 0o600 });
|
|
27
|
+
fs.renameSync(tmp, file);
|
|
28
|
+
}
|
|
29
|
+
function alive(pid) {
|
|
30
|
+
try {
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return err.code !== "ESRCH";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function ensureMeetingRiderJob(dir) {
|
|
39
|
+
return withJobsLock(dir, () => {
|
|
40
|
+
const jobs = readLocalJobsStrict(dir);
|
|
41
|
+
const existing = jobs.find((job) => job.context?.jobKind === MEETING_RIDER_JOB_KIND);
|
|
42
|
+
const marker = readBuiltinJobsMarker(dir);
|
|
43
|
+
if (marker.registered[MEETING_RIDER_JOB_KIND])
|
|
44
|
+
return existing ?? null;
|
|
45
|
+
const job = existing ?? {
|
|
46
|
+
jobId: `job_${randomUUID().replaceAll("-", "").slice(0, 20)}`,
|
|
47
|
+
prompt: "Ride an attended live meeting and extract incremental signals in-process.",
|
|
48
|
+
trigger: { kind: "event", event: "meeting.detected" }, timezone: "UTC",
|
|
49
|
+
context: { jobKind: MEETING_RIDER_JOB_KIND }, createdAt: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
if (!existing)
|
|
52
|
+
writeLocalJobs(dir, [...jobs, job]);
|
|
53
|
+
marker.registered[MEETING_RIDER_JOB_KIND] = new Date().toISOString();
|
|
54
|
+
writeBuiltinJobsMarker(dir, marker);
|
|
55
|
+
return job;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function meetingRiderSettings(hqRoot, home = os.homedir()) {
|
|
59
|
+
const enabled = executiveRiderEnabled(home);
|
|
60
|
+
let interval = 20;
|
|
61
|
+
try {
|
|
62
|
+
const settings = yaml.load(fs.readFileSync(path.join(hqRoot, "core/settings/signals.yaml"), "utf8"));
|
|
63
|
+
const value = settings?.rider?.interval_seconds;
|
|
64
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
65
|
+
interval = Math.max(5, value);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err.code !== "ENOENT")
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return { enabled, interval };
|
|
72
|
+
}
|
|
73
|
+
/** A crash after append but before removing the lock must not append the end event twice. */
|
|
74
|
+
function emitEndedOnce(dir, meeting, record) {
|
|
75
|
+
let payload = { ...meeting, source_ref: meetingSignalSourceRef(meeting.meeting_id), signal_ids: record.signals_written };
|
|
76
|
+
if (Buffer.byteLength(JSON.stringify(payload), "utf8") >= EVENT_PAYLOAD_BYTES) {
|
|
77
|
+
// The summary is durable before emit. Keep the complete IDs there rather
|
|
78
|
+
// than silently truncating them or stranding completion on the event cap.
|
|
79
|
+
payload = { ...payload, signal_ids: [], signal_count: record.signals_written.length,
|
|
80
|
+
signal_ids_ref: { path: `workspace/executive/meetings/${meeting.company}/${meeting.meeting_id}.rider.json`, field: "signals_written" } };
|
|
81
|
+
}
|
|
82
|
+
emitBotEvent(dir, "meeting.ended", payload, new Date(), (event) => event.event === "meeting.ended" && event.payload.company === meeting.company && event.payload.meeting_id === meeting.meeting_id);
|
|
83
|
+
}
|
|
84
|
+
export function createMeetingRider(deps) {
|
|
85
|
+
const now = deps.now ?? (() => new Date());
|
|
86
|
+
const running = new Map();
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const root = path.join(deps.hqRoot, "workspace/executive/meetings");
|
|
89
|
+
const emit = deps.emit ?? ((meeting, record) => emitEndedOnce(deps.dir, meeting, record));
|
|
90
|
+
const paths = (meeting) => {
|
|
91
|
+
assertSafeSegment(meeting.company, "company");
|
|
92
|
+
assertSafeSegment(meeting.meeting_id, "meeting id");
|
|
93
|
+
const base = path.join(root, meeting.company, meeting.meeting_id);
|
|
94
|
+
return { lock: `${base}.rider.lock`, result: `${base}.rider.json` };
|
|
95
|
+
};
|
|
96
|
+
const persist = (state) => atomic(paths(state).lock, state);
|
|
97
|
+
const finish = (state) => {
|
|
98
|
+
const { lock, result } = paths(state);
|
|
99
|
+
const record = { started_at: state.started_at, ended_at: state.ended_at, passes: state.passes, signals_written: state.signals_written, lanes: state.lanes };
|
|
100
|
+
atomic(result, record);
|
|
101
|
+
emit({ company: state.company, meeting_id: state.meeting_id }, record);
|
|
102
|
+
fs.unlinkSync(lock);
|
|
103
|
+
};
|
|
104
|
+
const loop = async (state, resumed) => {
|
|
105
|
+
const file = paths(state).lock;
|
|
106
|
+
if (state.ended_at) {
|
|
107
|
+
finish(state);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let first = !resumed || state.passes === 0;
|
|
111
|
+
while (!controller.signal.aborted) {
|
|
112
|
+
if (!deps.enabled()) {
|
|
113
|
+
deps.log("rider disabled");
|
|
114
|
+
fs.unlinkSync(file);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const started = now().getTime();
|
|
118
|
+
const status = await deps.live({ meeting_id: state.meeting_id, company: state.company });
|
|
119
|
+
if (controller.signal.aborted)
|
|
120
|
+
return;
|
|
121
|
+
if (!status.attended) {
|
|
122
|
+
deps.log(`rider skipped ${state.meeting_id}: owner did not attend`);
|
|
123
|
+
fs.unlinkSync(file);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
state.end_polls = terminal(status.status) ? state.end_polls + 1 : 0;
|
|
127
|
+
// A detection delivered after the meeting ended needs just one final pass.
|
|
128
|
+
const ended = (first && terminal(status.status)) || state.end_polls >= END_POLLS;
|
|
129
|
+
first = false;
|
|
130
|
+
const pass = await deps.runIncremental({ meeting_id: state.meeting_id, company: state.company });
|
|
131
|
+
if (pass.skipped) {
|
|
132
|
+
deps.log(`rider ${state.meeting_id} waiting for the signals lock`);
|
|
133
|
+
// Keep a pending final pass pending even when the first read was terminal.
|
|
134
|
+
if (ended)
|
|
135
|
+
state.end_polls = END_POLLS;
|
|
136
|
+
persist(state);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
state.passes++;
|
|
140
|
+
state.signals_written = [...new Set([...state.signals_written, ...pass.signal_ids])];
|
|
141
|
+
state.lanes = [...new Set([...state.lanes, ...(pass.lanes ?? [])])];
|
|
142
|
+
deps.log(`rider ${state.meeting_id} pass ${state.passes}`);
|
|
143
|
+
// Persist progress even on a stop; the next process resumes this cursor.
|
|
144
|
+
persist(state);
|
|
145
|
+
if (controller.signal.aborted)
|
|
146
|
+
return;
|
|
147
|
+
if (ended) {
|
|
148
|
+
if (!deps.enabled()) {
|
|
149
|
+
deps.log("rider disabled");
|
|
150
|
+
fs.unlinkSync(file);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
state.ended_at = now().toISOString();
|
|
154
|
+
persist(state);
|
|
155
|
+
finish(state);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const seconds = deps.intervalSeconds?.() ?? 20;
|
|
160
|
+
const cadence = Number.isFinite(seconds) ? Math.max(5, seconds) * 1000 : 20_000;
|
|
161
|
+
await (deps.sleep ?? ((ms, signal) => delay(ms, undefined, { signal })))(Math.max(0, cadence - (now().getTime() - started)), controller.signal);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const launch = (state, resumed) => {
|
|
165
|
+
const key = paths(state).lock;
|
|
166
|
+
const task = loop(state, resumed).catch(() => {
|
|
167
|
+
if (!controller.signal.aborted)
|
|
168
|
+
deps.log(`rider ${state.meeting_id} interrupted; retained lock for restart`);
|
|
169
|
+
}).finally(() => running.delete(key));
|
|
170
|
+
running.set(key, task);
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
/** Synchronously persists intent before the event handler may acknowledge delivery. */
|
|
174
|
+
start(payload) {
|
|
175
|
+
if (!deps.enabled()) {
|
|
176
|
+
deps.log("rider disabled");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!payload || typeof payload !== "object")
|
|
180
|
+
throw new Error("Invalid meeting.detected payload");
|
|
181
|
+
const { company, meeting_id } = payload;
|
|
182
|
+
if (typeof company !== "string" || typeof meeting_id !== "string")
|
|
183
|
+
throw new Error("meeting.detected needs company and meeting_id");
|
|
184
|
+
const meeting = { company, meeting_id };
|
|
185
|
+
const { lock, result } = paths(meeting);
|
|
186
|
+
if (fs.existsSync(result) && !fs.existsSync(lock))
|
|
187
|
+
return;
|
|
188
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
189
|
+
const state = { ...meeting, bot: deps.bot, pid: process.pid, started_at: now().toISOString(), passes: 0, end_polls: 0, signals_written: [], lanes: [] };
|
|
190
|
+
// Publish a complete intent atomically without replacing an existing lock.
|
|
191
|
+
const intent = `${lock}.${randomUUID()}.tmp`;
|
|
192
|
+
fs.writeFileSync(intent, JSON.stringify(state), { mode: 0o600 });
|
|
193
|
+
try {
|
|
194
|
+
fs.linkSync(intent, lock);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err.code !== "EEXIST")
|
|
198
|
+
throw err;
|
|
199
|
+
deps.log(`rider already running: ${meeting_id}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
fs.unlinkSync(intent);
|
|
204
|
+
}
|
|
205
|
+
launch(state, false);
|
|
206
|
+
},
|
|
207
|
+
resume() {
|
|
208
|
+
if (!deps.enabled()) {
|
|
209
|
+
deps.log("rider disabled");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!fs.existsSync(root))
|
|
213
|
+
return;
|
|
214
|
+
for (const company of fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory())) {
|
|
215
|
+
for (const name of fs.readdirSync(path.join(root, company.name)).filter((name) => name.endsWith(".rider.lock"))) {
|
|
216
|
+
const file = path.join(root, company.name, name);
|
|
217
|
+
try {
|
|
218
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
219
|
+
if (state.bot !== deps.bot || paths(state).lock !== file || running.has(file))
|
|
220
|
+
continue;
|
|
221
|
+
if ((deps.isAlive ?? alive)(state.pid))
|
|
222
|
+
continue;
|
|
223
|
+
if (now().getTime() - fs.statSync(file).mtimeMs >= MAX_LOCK_AGE) {
|
|
224
|
+
fs.unlinkSync(file);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
state.pid = process.pid;
|
|
228
|
+
persist(state);
|
|
229
|
+
launch(state, true);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
deps.log(`rider lock could not be resumed: ${name}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
async settled() { await Promise.all(running.values()); },
|
|
238
|
+
async stop() { controller.abort(); await Promise.all(running.values()); },
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Production dependencies use the same attended endpoint as `meetings live`. */
|
|
242
|
+
export function defaultMeetingRiderDeps(config, dir, log) {
|
|
243
|
+
return {
|
|
244
|
+
hqRoot: config.hqRoot, dir, bot: config.name, log,
|
|
245
|
+
enabled: () => meetingRiderSettings(config.hqRoot).enabled,
|
|
246
|
+
intervalSeconds: () => meetingRiderSettings(config.hqRoot).interval,
|
|
247
|
+
live: async ({ company, meeting_id }) => {
|
|
248
|
+
const token = await ensureCognitoToken();
|
|
249
|
+
const companyId = await getCompanyUid(token, company);
|
|
250
|
+
const response = await vaultApiFetch({ token, path: "/v1/meetings", query: { companyId, view: "live" } });
|
|
251
|
+
if (!response.ok)
|
|
252
|
+
throw new Error("Could not check attended meetings");
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
if (!Array.isArray(data.bots))
|
|
255
|
+
throw new Error("Invalid attended meetings response");
|
|
256
|
+
const row = data.bots.find((bot) => bot && typeof bot === "object" &&
|
|
257
|
+
(bot.companyId === undefined || bot.companyId === companyId) &&
|
|
258
|
+
(bot.meetingId === meeting_id || bot.recallBotId === meeting_id || bot.botId === meeting_id));
|
|
259
|
+
if (row && typeof row.status !== "string")
|
|
260
|
+
throw new Error("Invalid attended meeting status");
|
|
261
|
+
return { attended: Boolean(row), status: typeof row?.status === "string" ? row.status : "unknown" };
|
|
262
|
+
},
|
|
263
|
+
runIncremental: async ({ company, meeting_id }) => {
|
|
264
|
+
const source = meetingSignalSourceRef(meeting_id);
|
|
265
|
+
const result = await runSignals({ hqRoot: config.hqRoot, company, source, incremental: true, trigger: "manual",
|
|
266
|
+
incrementalOptions: { onSignalWritten: async ({ event, ...payload }) => { emitBotEvent(dir, event, payload); } },
|
|
267
|
+
});
|
|
268
|
+
if (result.locked?.some((record) => record.company === company))
|
|
269
|
+
return { signal_ids: [], skipped: true };
|
|
270
|
+
if (!result.records.length || result.records.some((record) => record.status !== "complete"))
|
|
271
|
+
throw new Error("Incremental pass did not complete");
|
|
272
|
+
const file = path.join(config.hqRoot, cursorPath(company, source));
|
|
273
|
+
const cursor = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : { signal_ids: [] };
|
|
274
|
+
// US-003 adds signal metadata to lane records. Reading it is additive:
|
|
275
|
+
// older lanes have none and are not included in this source's summary.
|
|
276
|
+
const lanes = readAllLanes(config.hqRoot).filter((lane) => {
|
|
277
|
+
const signal = lane.signal;
|
|
278
|
+
return lane.company?.slug === company && signal?.source_ref === source;
|
|
279
|
+
}).map((lane) => lane.lane_id);
|
|
280
|
+
return { signal_ids: cursor.signal_ids, lanes };
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=meeting-rider.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type BotConfig } from "../config.js";
|
|
2
|
+
import { type LocalJob } from "../local-jobs.js";
|
|
3
|
+
import type { JobRunCompletion } from "../api.js";
|
|
4
|
+
export declare const SIGNAL_TO_LANE_JOB_KIND = "signal-to-lane";
|
|
5
|
+
export declare function signalLaneCompanies(config: BotConfig): string[];
|
|
6
|
+
export declare function ensureSignalToLaneJobs(dir: string, config: BotConfig, companies?: string[], home?: string): LocalJob[];
|
|
7
|
+
/** Only new locally extracted signals reach here; cloud upload/garden is separate. */
|
|
8
|
+
export declare function emitExecutiveSignalWritten(hqRoot: string, company: string, payload: Record<string, string>, root?: string, home?: string): boolean;
|
|
9
|
+
/** Persist intent before publishing a new signal file; ordinary redo creates none. */
|
|
10
|
+
export declare function prepareSignalPublication(hqRoot: string, company: string, payload: Record<string, string>): void;
|
|
11
|
+
/** A pre-append crash retries; a post-append crash uses the event journal's dedupe. */
|
|
12
|
+
export declare function flushSignalPublications(hqRoot: string, company: string, emit?: typeof emitExecutiveSignalWritten): void;
|
|
13
|
+
type Create = (input: {
|
|
14
|
+
hqRoot: string;
|
|
15
|
+
company: string;
|
|
16
|
+
signal_id: string;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
lane_id: string;
|
|
19
|
+
existing: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function runSignalToLane(input: {
|
|
22
|
+
dir: string;
|
|
23
|
+
config: BotConfig;
|
|
24
|
+
payload: unknown;
|
|
25
|
+
company: unknown;
|
|
26
|
+
log: (message: string) => void;
|
|
27
|
+
create?: Create;
|
|
28
|
+
companies?: string[];
|
|
29
|
+
home?: string;
|
|
30
|
+
}): Promise<JobRunCompletion>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=signal-to-lane.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Source-independent action-item consumer for the personal Executive. */
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { readBotConfig } from "../config.js";
|
|
6
|
+
import { botsRoot } from "../paths.js";
|
|
7
|
+
import { fleetProfileOwnerCompanies } from "../fleet-local.js";
|
|
8
|
+
import { fleetHotSignalSyncCompanies } from "../fleet-cron.js";
|
|
9
|
+
import { readActiveCompanySlug, readBuiltinJobsMarker, writeBuiltinJobsMarker } from "./hot-signal-sync.js";
|
|
10
|
+
import { isExecutiveBot } from "./meeting-rider.js";
|
|
11
|
+
import { readLocalJobsStrict, writeLocalJobs, withJobsLock } from "../local-jobs.js";
|
|
12
|
+
import { emitBotEvent } from "../events.js";
|
|
13
|
+
import { isLanesError } from "../../lanes/errors.js";
|
|
14
|
+
import { lookupSignal } from "../../lanes/signal-lane.js";
|
|
15
|
+
export const SIGNAL_TO_LANE_JOB_KIND = "signal-to-lane";
|
|
16
|
+
export function signalLaneCompanies(config) {
|
|
17
|
+
return fleetHotSignalSyncCompanies(config, readActiveCompanySlug(config.hqRoot) ?? null, fleetProfileOwnerCompanies(config.agentUid));
|
|
18
|
+
}
|
|
19
|
+
export function ensureSignalToLaneJobs(dir, config, companies = signalLaneCompanies(config), home) {
|
|
20
|
+
if (!isExecutiveBot(config, home))
|
|
21
|
+
return [];
|
|
22
|
+
return withJobsLock(dir, () => {
|
|
23
|
+
const jobs = readLocalJobsStrict(dir);
|
|
24
|
+
const marker = readBuiltinJobsMarker(dir);
|
|
25
|
+
for (const company of new Set(companies)) {
|
|
26
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(company) || company === "personal")
|
|
27
|
+
continue;
|
|
28
|
+
const key = `${SIGNAL_TO_LANE_JOB_KIND}:${company}`;
|
|
29
|
+
if (marker.registered[key])
|
|
30
|
+
continue;
|
|
31
|
+
if (!jobs.some(job => job.context?.jobKind === SIGNAL_TO_LANE_JOB_KIND && job.context.company === company)) {
|
|
32
|
+
jobs.push({ jobId: `job_${randomUUID().replaceAll("-", "").slice(0, 20)}`,
|
|
33
|
+
prompt: "Open a conductor lane from an action item in-process.", timezone: "UTC",
|
|
34
|
+
trigger: { kind: "event", event: "signal.written", filter: { type: "action_item", company } },
|
|
35
|
+
context: { jobKind: SIGNAL_TO_LANE_JOB_KIND, company }, createdAt: new Date().toISOString() });
|
|
36
|
+
}
|
|
37
|
+
marker.registered[key] = new Date().toISOString();
|
|
38
|
+
}
|
|
39
|
+
writeLocalJobs(dir, jobs);
|
|
40
|
+
writeBuiltinJobsMarker(dir, marker);
|
|
41
|
+
return jobs.filter(job => job.context?.jobKind === SIGNAL_TO_LANE_JOB_KIND);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Only new locally extracted signals reach here; cloud upload/garden is separate. */
|
|
45
|
+
export function emitExecutiveSignalWritten(hqRoot, company, payload, root = botsRoot(), home) {
|
|
46
|
+
if (!fs.existsSync(root))
|
|
47
|
+
return false;
|
|
48
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isDirectory())
|
|
50
|
+
continue;
|
|
51
|
+
const dir = path.join(root, entry.name);
|
|
52
|
+
const config = readBotConfig(dir);
|
|
53
|
+
if (!config || typeof config.hqRoot !== "string" || path.resolve(config.hqRoot) !== path.resolve(hqRoot) || !isExecutiveBot(config, home))
|
|
54
|
+
continue;
|
|
55
|
+
if (!signalLaneCompanies(config).includes(company))
|
|
56
|
+
continue;
|
|
57
|
+
emitBotEvent(dir, "signal.written", { ...payload, company }, new Date(), prior => prior.event === "signal.written" && prior.payload.company === company && prior.payload.signal_id === payload.signal_id);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function publicationDir(hqRoot, company) {
|
|
63
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(company))
|
|
64
|
+
throw new Error("Invalid signal publication company");
|
|
65
|
+
return path.join(hqRoot, "workspace", "signal-publications", company);
|
|
66
|
+
}
|
|
67
|
+
/** Persist intent before publishing a new signal file; ordinary redo creates none. */
|
|
68
|
+
export function prepareSignalPublication(hqRoot, company, payload) {
|
|
69
|
+
const dir = publicationDir(hqRoot, company);
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
71
|
+
const file = path.join(dir, createHash("sha256").update(payload.signal_id).digest("hex") + ".json");
|
|
72
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
73
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), { mode: 0o600 });
|
|
74
|
+
fs.renameSync(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
/** A pre-append crash retries; a post-append crash uses the event journal's dedupe. */
|
|
77
|
+
export function flushSignalPublications(hqRoot, company, emit = emitExecutiveSignalWritten) {
|
|
78
|
+
const dir = publicationDir(hqRoot, company);
|
|
79
|
+
if (!fs.existsSync(dir))
|
|
80
|
+
return;
|
|
81
|
+
for (const name of fs.readdirSync(dir).filter(name => /^[a-f0-9]{64}\.json$/.test(name))) {
|
|
82
|
+
const file = path.join(dir, name);
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code === "ENOENT")
|
|
89
|
+
continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) ||
|
|
93
|
+
["signal_id", "type", "source_ref", "audience_key", "origin"].some(key => typeof payload[key] !== "string")) {
|
|
94
|
+
throw new Error("Invalid signal publication intent");
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
lookupSignal(hqRoot, company, payload.signal_id);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (isLanesError(error) && error.error === "signal_not_found")
|
|
101
|
+
continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (emit(hqRoot, company, payload)) {
|
|
105
|
+
try {
|
|
106
|
+
fs.unlinkSync(file);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code !== "ENOENT")
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const createDefault = async (input) => {
|
|
116
|
+
// Load the CLI admission graph only when a signal actually needs a lane.
|
|
117
|
+
const { createLaneFromSignal } = await import("../../../commands/lanes.js");
|
|
118
|
+
return createLaneFromSignal(input);
|
|
119
|
+
};
|
|
120
|
+
export async function runSignalToLane(input) {
|
|
121
|
+
if (!isExecutiveBot(input.config, input.home))
|
|
122
|
+
return { status: "skipped-precondition", output: "Not the Executive" };
|
|
123
|
+
const payload = input.payload;
|
|
124
|
+
const company = input.company;
|
|
125
|
+
if (!payload || payload.type !== "action_item" || typeof payload.signal_id !== "string" || !payload.signal_id.trim() ||
|
|
126
|
+
typeof company !== "string" || payload.company !== company || !(input.companies ?? signalLaneCompanies(input.config)).includes(company)) {
|
|
127
|
+
return { status: "skipped-precondition", output: "Signal outside the served company or action-item scope" };
|
|
128
|
+
}
|
|
129
|
+
// Persist deferred IDs before acknowledging; the next matching event retries.
|
|
130
|
+
const file = path.join(input.dir, "signal-to-lane-pending.json");
|
|
131
|
+
const read = () => {
|
|
132
|
+
try {
|
|
133
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
135
|
+
Object.values(value).some(ids => !Array.isArray(ids) || ids.some(id => typeof id !== "string"))) {
|
|
136
|
+
throw new Error("Invalid pending signal lane state");
|
|
137
|
+
}
|
|
138
|
+
return Object.assign(Object.create(null), value);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err.code === "ENOENT")
|
|
142
|
+
return Object.create(null);
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const update = (change) => withJobsLock(input.dir, () => {
|
|
147
|
+
const pending = read();
|
|
148
|
+
change(pending);
|
|
149
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
150
|
+
fs.writeFileSync(tmp, JSON.stringify(pending), { mode: 0o600 });
|
|
151
|
+
fs.renameSync(tmp, file);
|
|
152
|
+
return pending;
|
|
153
|
+
});
|
|
154
|
+
const pending = update(p => { p[company] = [...new Set([...(p[company] ?? []), payload.signal_id])]; });
|
|
155
|
+
for (const signal_id of pending[company] ?? []) {
|
|
156
|
+
try {
|
|
157
|
+
const result = await (input.create ?? createDefault)({ hqRoot: input.config.hqRoot, company, signal_id });
|
|
158
|
+
input.log(JSON.stringify(result));
|
|
159
|
+
update(p => { p[company] = (p[company] ?? []).filter(id => id !== signal_id); });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (!isLanesError(err) || err.error !== "no_conductor_link")
|
|
163
|
+
throw err;
|
|
164
|
+
input.log(JSON.stringify({ error: "no_conductor_link", company, signal_id }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { status: "succeeded", output: "Signal lane processing finished; deferred signals retry on the next matching event" };
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=signal-to-lane.js.map
|
|
@@ -23,7 +23,12 @@ export interface LocalJob {
|
|
|
23
23
|
jobId: string;
|
|
24
24
|
prompt: string;
|
|
25
25
|
/** Standard 5-field cron, evaluated in `timezone`. */
|
|
26
|
-
cron
|
|
26
|
+
cron?: string;
|
|
27
|
+
trigger?: {
|
|
28
|
+
kind: "event";
|
|
29
|
+
event: string;
|
|
30
|
+
filter?: Record<string, string>;
|
|
31
|
+
};
|
|
27
32
|
timezone: string;
|
|
28
33
|
context?: Record<string, unknown>;
|
|
29
34
|
createdAt: string;
|
|
@@ -45,6 +50,12 @@ export interface LocalJob {
|
|
|
45
50
|
runCount?: number;
|
|
46
51
|
}
|
|
47
52
|
export declare function readLocalJobs(dir: string): LocalJob[];
|
|
53
|
+
/**
|
|
54
|
+
* `jobs.json` for a write that adds a job: a missing file is an empty list,
|
|
55
|
+
* but a file that exists and cannot be read or parsed throws, so adding a job
|
|
56
|
+
* never writes back a list that drops every other job.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readLocalJobsStrict(dir: string): LocalJob[];
|
|
48
59
|
export declare function writeLocalJobs(dir: string, jobs: LocalJob[]): void;
|
|
49
60
|
/**
|
|
50
61
|
* Run a read-change-write of `jobs.json` under an exclusive lock. The running
|
|
@@ -74,9 +85,11 @@ export declare function parseCron(expr: string): CronMatcher | null;
|
|
|
74
85
|
export declare function latestDueFire(job: LocalJob, now: Date): Date | null;
|
|
75
86
|
/** The next time this job is due after `now`, within a week; null otherwise. */
|
|
76
87
|
export declare function nextDueFire(job: LocalJob, now: Date): Date | null;
|
|
88
|
+
export declare function toJobRecord(job: LocalJob, now?: Date): BotJobRecord;
|
|
77
89
|
export declare function addLocalJob(dir: string, input: {
|
|
78
90
|
prompt: string;
|
|
79
|
-
cron
|
|
91
|
+
cron?: string;
|
|
92
|
+
trigger?: LocalJob["trigger"];
|
|
80
93
|
timezone: string;
|
|
81
94
|
context?: Record<string, unknown>;
|
|
82
95
|
}, now?: Date): LocalJob;
|
|
@@ -22,6 +22,7 @@ import * as fs from "node:fs";
|
|
|
22
22
|
import * as path from "node:path";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import { BotApiError } from "./api.js";
|
|
25
|
+
import { ackBotEvent } from "./events.js";
|
|
25
26
|
const LOCAL_JOBS_FILE = "jobs.json";
|
|
26
27
|
const LOCAL_JOB_MESSAGE_PREFIX = "localjob-";
|
|
27
28
|
/** How far back a missed fire is still looked for (one week). */
|
|
@@ -47,7 +48,7 @@ export function readLocalJobs(dir) {
|
|
|
47
48
|
* but a file that exists and cannot be read or parsed throws, so adding a job
|
|
48
49
|
* never writes back a list that drops every other job.
|
|
49
50
|
*/
|
|
50
|
-
function
|
|
51
|
+
export function readLocalJobsStrict(dir) {
|
|
51
52
|
let text;
|
|
52
53
|
try {
|
|
53
54
|
text = fs.readFileSync(localJobsPath(dir), "utf8");
|
|
@@ -270,6 +271,8 @@ function scheduleOrigin(job) {
|
|
|
270
271
|
return Date.parse(job.scheduledFrom ?? job.createdAt);
|
|
271
272
|
}
|
|
272
273
|
export function latestDueFire(job, now) {
|
|
274
|
+
if (job.trigger || !job.cron)
|
|
275
|
+
return null;
|
|
273
276
|
const interval = rateIntervalMs(job.cron);
|
|
274
277
|
if (interval) {
|
|
275
278
|
const origin = scheduleOrigin(job);
|
|
@@ -294,6 +297,8 @@ export function latestDueFire(job, now) {
|
|
|
294
297
|
}
|
|
295
298
|
/** The next time this job is due after `now`, within a week; null otherwise. */
|
|
296
299
|
export function nextDueFire(job, now) {
|
|
300
|
+
if (job.trigger || !job.cron)
|
|
301
|
+
return null;
|
|
297
302
|
const interval = rateIntervalMs(job.cron);
|
|
298
303
|
if (interval) {
|
|
299
304
|
const origin = scheduleOrigin(job);
|
|
@@ -313,13 +318,13 @@ export function nextDueFire(job, now) {
|
|
|
313
318
|
return null;
|
|
314
319
|
}
|
|
315
320
|
// ─── The job list (what `hq bot jobs` reads and writes) ──────────────────────
|
|
316
|
-
function toJobRecord(job, now = new Date()) {
|
|
321
|
+
export function toJobRecord(job, now = new Date()) {
|
|
317
322
|
const next = nextDueFire(job, now);
|
|
318
323
|
return {
|
|
319
324
|
jobId: job.jobId,
|
|
320
325
|
prompt: job.prompt,
|
|
321
326
|
status: "active",
|
|
322
|
-
schedule: { kind: "recurring", cron: job.cron, timezone: job.timezone },
|
|
327
|
+
...(job.trigger ? { trigger: job.trigger } : { schedule: { kind: "recurring", cron: job.cron, timezone: job.timezone } }),
|
|
323
328
|
...(job.context ? { context: job.context } : {}),
|
|
324
329
|
createdAt: job.createdAt,
|
|
325
330
|
...(next ? { nextRunAt: next.toISOString() } : {}),
|
|
@@ -329,17 +334,24 @@ function toJobRecord(job, now = new Date()) {
|
|
|
329
334
|
};
|
|
330
335
|
}
|
|
331
336
|
export function addLocalJob(dir, input, now = new Date()) {
|
|
332
|
-
if (!
|
|
337
|
+
if (input.trigger && (!input.trigger.event.trim() || input.trigger.kind !== "event" || input.cron))
|
|
338
|
+
throw new Error("An event job needs an event and no cron schedule.");
|
|
339
|
+
if (!input.trigger && (!input.cron || !isRunnableSchedule(input.cron)))
|
|
333
340
|
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
334
341
|
const job = {
|
|
335
342
|
jobId: `job_${randomUUID().replace(/-/g, "").slice(0, 20)}`,
|
|
336
343
|
prompt: input.prompt,
|
|
337
|
-
cron: input.cron,
|
|
344
|
+
...(input.trigger ? { trigger: input.trigger } : { cron: input.cron }),
|
|
338
345
|
timezone: input.timezone,
|
|
339
346
|
...(input.context ? { context: input.context } : {}),
|
|
340
347
|
createdAt: now.toISOString(),
|
|
341
348
|
};
|
|
342
|
-
withJobsLock(dir, () =>
|
|
349
|
+
withJobsLock(dir, () => {
|
|
350
|
+
const jobs = readLocalJobsStrict(dir);
|
|
351
|
+
if (jobs.length >= MAX_LOCAL_JOBS)
|
|
352
|
+
throw new BotApiError(409, "local jobs", "JOB_QUOTA_EXCEEDED", `at most ${MAX_LOCAL_JOBS} jobs`);
|
|
353
|
+
writeLocalJobs(dir, [...jobs, job]);
|
|
354
|
+
});
|
|
343
355
|
return job;
|
|
344
356
|
}
|
|
345
357
|
/**
|
|
@@ -350,7 +362,7 @@ export function ensureLocalJob(dir, matches, input, now = new Date()) {
|
|
|
350
362
|
if (!isRunnableSchedule(input.cron))
|
|
351
363
|
throw new Error(`"${input.cron}" is not a schedule this bot can run.`);
|
|
352
364
|
return withJobsLock(dir, () => {
|
|
353
|
-
const jobs =
|
|
365
|
+
const jobs = readLocalJobsStrict(dir);
|
|
354
366
|
const existing = jobs.find(matches);
|
|
355
367
|
if (existing)
|
|
356
368
|
return { job: existing, created: false };
|
|
@@ -386,6 +398,7 @@ schedule, now = new Date()) {
|
|
|
386
398
|
timezone: schedule.timezone ?? jobs[index].timezone,
|
|
387
399
|
scheduledFrom: now.toISOString(),
|
|
388
400
|
};
|
|
401
|
+
delete updated.trigger;
|
|
389
402
|
jobs[index] = updated;
|
|
390
403
|
writeLocalJobs(dir, jobs);
|
|
391
404
|
return updated;
|
|
@@ -517,6 +530,10 @@ export function withLocalJobs(api, dir, clock = () => new Date()) {
|
|
|
517
530
|
return [...cloud, ...takeDueLocalJobRuns(dir, clock())];
|
|
518
531
|
};
|
|
519
532
|
local.ackInbox = async (agentUid, messageId) => {
|
|
533
|
+
if (messageId.startsWith("localjob-event-")) {
|
|
534
|
+
ackBotEvent(dir, messageId);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
520
537
|
if (isLocalJobMessage(messageId))
|
|
521
538
|
return;
|
|
522
539
|
await api.ackInbox(agentUid, messageId);
|