@ouro.bot/cli 0.1.0-alpha.43 → 0.1.0-alpha.45
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.json +14 -0
- package/dist/heart/daemon/daemon-cli.js +35 -0
- package/dist/heart/daemon/launchd.js +21 -4
- package/dist/heart/daemon/sense-manager.js +26 -2
- package/dist/heart/daemon/subagent-installer.js +38 -6
- package/dist/senses/bluebubbles-client.js +51 -1
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-mutation-log.js +42 -0
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles.js +177 -3
- package/package.json +1 -1
- package/subagents/README.md +33 -7
- package/subagents/work-doer.md +22 -20
- package/subagents/work-planner.md +14 -6
package/changelog.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.45",
|
|
6
|
+
"changes": [
|
|
7
|
+
"`ouro up` now persists a boot-time launch agent with `RunAtLoad`, so the daemon comes back reliably after reboot instead of only starting for the current session.",
|
|
8
|
+
"BlueBubbles sense status is now truthful about upstream health: it probes the real BlueBubbles API, records runtime state, and surfaces `error` when the webhook listener is alive but the upstream server is unreachable.",
|
|
9
|
+
"BlueBubbles intake now replays recoverable read/delivery mutation backlogs through the normal inbound agent path, with a small dedupe ledger so missed messages can be recovered without silent drops or duplicate delivery."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.44",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Workflow skills now install into `~/.agents/skills` for Codex/OpenAI instead of duplicating installs under both `.agents` and `.codex`, which prevents duplicate advertised skills while keeping re-installs safe."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
4
18
|
{
|
|
5
19
|
"version": "0.1.0-alpha.43",
|
|
6
20
|
"changes": [
|
|
@@ -63,6 +63,7 @@ const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
|
63
63
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
64
64
|
const tasks_1 = require("../../repertoire/tasks");
|
|
65
65
|
const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
|
|
66
|
+
const launchd_1 = require("./launchd");
|
|
66
67
|
function stringField(value) {
|
|
67
68
|
return typeof value === "string" ? value : null;
|
|
68
69
|
}
|
|
@@ -761,6 +762,25 @@ function defaultFallbackPendingMessage(command) {
|
|
|
761
762
|
});
|
|
762
763
|
return pendingPath;
|
|
763
764
|
}
|
|
765
|
+
function defaultEnsureDaemonBootPersistence(socketPath) {
|
|
766
|
+
if (process.platform !== "darwin") {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const homeDir = os.homedir();
|
|
770
|
+
const launchdDeps = {
|
|
771
|
+
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
|
|
772
|
+
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
773
|
+
homeDir,
|
|
774
|
+
};
|
|
775
|
+
const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
|
|
776
|
+
const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
|
|
777
|
+
(0, launchd_1.writeLaunchAgentPlist)(launchdDeps, {
|
|
778
|
+
nodePath: process.execPath,
|
|
779
|
+
entryPath,
|
|
780
|
+
socketPath,
|
|
781
|
+
logDir,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
764
784
|
async function defaultInstallSubagents() {
|
|
765
785
|
return (0, subagent_installer_1.installSubagentsForAvailableCli)({
|
|
766
786
|
repoRoot: (0, identity_1.getRepoRoot)(),
|
|
@@ -1045,6 +1065,7 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
|
|
|
1045
1065
|
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
1046
1066
|
installOuroCommand: ouro_path_installer_1.installOuroCommand,
|
|
1047
1067
|
syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
|
|
1068
|
+
ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
|
|
1048
1069
|
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
1049
1070
|
startChat: async (agentName) => {
|
|
1050
1071
|
const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
@@ -1406,6 +1427,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
1406
1427
|
});
|
|
1407
1428
|
if (command.kind === "daemon.up") {
|
|
1408
1429
|
await performSystemSetup(deps);
|
|
1430
|
+
if (deps.ensureDaemonBootPersistence) {
|
|
1431
|
+
try {
|
|
1432
|
+
await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
(0, runtime_1.emitNervesEvent)({
|
|
1436
|
+
level: "warn",
|
|
1437
|
+
component: "daemon",
|
|
1438
|
+
event: "daemon.system_setup_launchd_error",
|
|
1439
|
+
message: "failed to persist daemon boot startup",
|
|
1440
|
+
meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1409
1444
|
// Run update hooks before starting daemon so user sees the output
|
|
1410
1445
|
(0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
|
|
1411
1446
|
const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DAEMON_PLIST_LABEL = void 0;
|
|
37
37
|
exports.generateDaemonPlist = generateDaemonPlist;
|
|
38
|
+
exports.writeLaunchAgentPlist = writeLaunchAgentPlist;
|
|
38
39
|
exports.installLaunchAgent = installLaunchAgent;
|
|
39
40
|
exports.uninstallLaunchAgent = uninstallLaunchAgent;
|
|
40
41
|
exports.isDaemonInstalled = isDaemonInstalled;
|
|
@@ -65,6 +66,8 @@ function generateDaemonPlist(options) {
|
|
|
65
66
|
` <string>--socket</string>`,
|
|
66
67
|
` <string>${options.socketPath}</string>`,
|
|
67
68
|
` </array>`,
|
|
69
|
+
` <key>RunAtLoad</key>`,
|
|
70
|
+
` <true/>`,
|
|
68
71
|
` <key>KeepAlive</key>`,
|
|
69
72
|
` <true/>`,
|
|
70
73
|
];
|
|
@@ -74,6 +77,23 @@ function generateDaemonPlist(options) {
|
|
|
74
77
|
lines.push(`</dict>`, `</plist>`, ``);
|
|
75
78
|
return lines.join("\n");
|
|
76
79
|
}
|
|
80
|
+
function writeLaunchAgentPlist(deps, options) {
|
|
81
|
+
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
82
|
+
deps.mkdirp(launchAgentsDir);
|
|
83
|
+
if (options.logDir) {
|
|
84
|
+
deps.mkdirp(options.logDir);
|
|
85
|
+
}
|
|
86
|
+
const fullPath = plistFilePath(deps.homeDir);
|
|
87
|
+
const xml = generateDaemonPlist(options);
|
|
88
|
+
deps.writeFile(fullPath, xml);
|
|
89
|
+
(0, runtime_1.emitNervesEvent)({
|
|
90
|
+
component: "daemon",
|
|
91
|
+
event: "daemon.launchd_plist_written",
|
|
92
|
+
message: "daemon launch agent plist written",
|
|
93
|
+
meta: { plistPath: fullPath, entryPath: options.entryPath, socketPath: options.socketPath },
|
|
94
|
+
});
|
|
95
|
+
return fullPath;
|
|
96
|
+
}
|
|
77
97
|
function installLaunchAgent(deps, options) {
|
|
78
98
|
(0, runtime_1.emitNervesEvent)({
|
|
79
99
|
component: "daemon",
|
|
@@ -81,8 +101,6 @@ function installLaunchAgent(deps, options) {
|
|
|
81
101
|
message: "installing launch agent",
|
|
82
102
|
meta: { entryPath: options.entryPath, socketPath: options.socketPath },
|
|
83
103
|
});
|
|
84
|
-
const launchAgentsDir = path.join(deps.homeDir, "Library", "LaunchAgents");
|
|
85
|
-
deps.mkdirp(launchAgentsDir);
|
|
86
104
|
const fullPath = plistFilePath(deps.homeDir);
|
|
87
105
|
// Unload existing (best effort) for idempotent re-install
|
|
88
106
|
if (deps.existsFile(fullPath)) {
|
|
@@ -91,8 +109,7 @@ function installLaunchAgent(deps, options) {
|
|
|
91
109
|
}
|
|
92
110
|
catch { /* best effort */ }
|
|
93
111
|
}
|
|
94
|
-
|
|
95
|
-
deps.writeFile(fullPath, xml);
|
|
112
|
+
writeLaunchAgentPlist(deps, options);
|
|
96
113
|
deps.exec(`launchctl load "${fullPath}"`);
|
|
97
114
|
(0, runtime_1.emitNervesEvent)({
|
|
98
115
|
component: "daemon",
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const os = __importStar(require("os"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
const bluebubbles_runtime_state_1 = require("../../senses/bluebubbles-runtime-state");
|
|
41
42
|
const identity_1 = require("../identity");
|
|
42
43
|
const sense_truth_1 = require("../sense-truth");
|
|
43
44
|
const process_manager_1 = require("./process-manager");
|
|
@@ -175,12 +176,29 @@ function runtimeInfoFor(status) {
|
|
|
175
176
|
return { runtime: "running" };
|
|
176
177
|
return { runtime: "error" };
|
|
177
178
|
}
|
|
179
|
+
function readBlueBubblesRuntimeFacts(agent, bundlesRoot, snapshot) {
|
|
180
|
+
const agentRoot = path.join(bundlesRoot, `${agent}.ouro`);
|
|
181
|
+
const runtimePath = path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
|
|
182
|
+
if (snapshot?.runtime !== "running" || !fs.existsSync(runtimePath)) {
|
|
183
|
+
return { runtime: snapshot?.runtime };
|
|
184
|
+
}
|
|
185
|
+
const state = (0, bluebubbles_runtime_state_1.readBlueBubblesRuntimeState)(agent, agentRoot);
|
|
186
|
+
if (state.upstreamStatus === "error") {
|
|
187
|
+
return {
|
|
188
|
+
runtime: "error",
|
|
189
|
+
detail: state.detail,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return { runtime: snapshot.runtime };
|
|
193
|
+
}
|
|
178
194
|
class DaemonSenseManager {
|
|
179
195
|
processManager;
|
|
180
196
|
contexts;
|
|
197
|
+
bundlesRoot;
|
|
181
198
|
constructor(options) {
|
|
182
199
|
const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
|
|
183
200
|
const secretsRoot = options.secretsRoot ?? path.join(os.homedir(), ".agentsecrets");
|
|
201
|
+
this.bundlesRoot = bundlesRoot;
|
|
184
202
|
this.contexts = new Map(options.agents.map((agent) => {
|
|
185
203
|
const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
|
|
186
204
|
const facts = senseFactsFromSecrets(agent, senses, path.join(secretsRoot, agent, "secrets.json"));
|
|
@@ -227,6 +245,7 @@ class DaemonSenseManager {
|
|
|
227
245
|
runtime.set(parsed.agent, current);
|
|
228
246
|
}
|
|
229
247
|
const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
|
|
248
|
+
const blueBubblesRuntimeFacts = readBlueBubblesRuntimeFacts(agent, this.bundlesRoot, runtime.get(agent)?.bluebubbles);
|
|
230
249
|
const runtimeInfo = {
|
|
231
250
|
cli: { configured: true },
|
|
232
251
|
teams: {
|
|
@@ -235,7 +254,7 @@ class DaemonSenseManager {
|
|
|
235
254
|
},
|
|
236
255
|
bluebubbles: {
|
|
237
256
|
configured: context.facts.bluebubbles.configured,
|
|
238
|
-
...
|
|
257
|
+
...blueBubblesRuntimeFacts,
|
|
239
258
|
},
|
|
240
259
|
};
|
|
241
260
|
const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
|
|
@@ -245,7 +264,12 @@ class DaemonSenseManager {
|
|
|
245
264
|
label: entry.label,
|
|
246
265
|
enabled: entry.enabled,
|
|
247
266
|
status: entry.status,
|
|
248
|
-
detail: entry.enabled
|
|
267
|
+
detail: entry.enabled
|
|
268
|
+
? entry.sense === "bluebubbles"
|
|
269
|
+
? blueBubblesRuntimeFacts.detail
|
|
270
|
+
?? context.facts[entry.sense].detail
|
|
271
|
+
: context.facts[entry.sense].detail
|
|
272
|
+
: "not enabled in agent.json",
|
|
249
273
|
}));
|
|
250
274
|
});
|
|
251
275
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -64,6 +64,16 @@ function pathExists(target) {
|
|
|
64
64
|
return false;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
function isSameFile(source, target) {
|
|
68
|
+
try {
|
|
69
|
+
const sourceStats = fs.statSync(source);
|
|
70
|
+
const targetStats = fs.statSync(target);
|
|
71
|
+
return sourceStats.dev === targetStats.dev && sourceStats.ino === targetStats.ino;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
function ensureSymlink(source, target) {
|
|
68
78
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
69
79
|
if (pathExists(target)) {
|
|
@@ -78,6 +88,25 @@ function ensureSymlink(source, target) {
|
|
|
78
88
|
fs.symlinkSync(source, target);
|
|
79
89
|
return true;
|
|
80
90
|
}
|
|
91
|
+
function ensureHardLink(source, target) {
|
|
92
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
93
|
+
if (pathExists(target)) {
|
|
94
|
+
const stats = fs.lstatSync(target);
|
|
95
|
+
if (!stats.isSymbolicLink() && isSameFile(source, target)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
fs.unlinkSync(target);
|
|
99
|
+
}
|
|
100
|
+
fs.linkSync(source, target);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
function hasOpenAiSkillHome(homeDir) {
|
|
104
|
+
return pathExists(path.join(homeDir, ".codex")) || pathExists(path.join(homeDir, ".agents"));
|
|
105
|
+
}
|
|
106
|
+
function openAiSkillTargets(homeDir, source) {
|
|
107
|
+
const skillName = path.basename(source, ".md");
|
|
108
|
+
return [path.join(homeDir, ".agents", "skills", skillName, "SKILL.md")];
|
|
109
|
+
}
|
|
81
110
|
async function installSubagentsForAvailableCli(options = {}) {
|
|
82
111
|
const repoRoot = options.repoRoot ?? path.resolve(__dirname, "..", "..", "..");
|
|
83
112
|
const homeDir = options.homeDir ?? os.homedir();
|
|
@@ -111,15 +140,18 @@ async function installSubagentsForAvailableCli(options = {}) {
|
|
|
111
140
|
}
|
|
112
141
|
}
|
|
113
142
|
const codexPath = which("codex");
|
|
114
|
-
if (!codexPath) {
|
|
115
|
-
notes.push("codex CLI not found; skipping subagent install");
|
|
143
|
+
if (!codexPath && !hasOpenAiSkillHome(homeDir)) {
|
|
144
|
+
notes.push("codex CLI/config not found; skipping subagent install");
|
|
116
145
|
}
|
|
117
146
|
else {
|
|
118
|
-
const codexSkillsDir = path.join(homeDir, ".codex", "skills");
|
|
119
147
|
for (const source of sources) {
|
|
120
|
-
|
|
121
|
-
const target
|
|
122
|
-
|
|
148
|
+
let installedForSkill = false;
|
|
149
|
+
for (const target of openAiSkillTargets(homeDir, source)) {
|
|
150
|
+
if (ensureHardLink(source, target)) {
|
|
151
|
+
installedForSkill = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (installedForSkill) {
|
|
123
155
|
codexInstalled += 1;
|
|
124
156
|
}
|
|
125
157
|
}
|
|
@@ -170,6 +170,12 @@ function applyRepairNotice(event, notice) {
|
|
|
170
170
|
repairNotice: notice,
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
function hasRecoverableMessageContent(event) {
|
|
174
|
+
return event.kind === "message"
|
|
175
|
+
&& (event.textForAgent.trim().length > 0
|
|
176
|
+
|| event.attachments.length > 0
|
|
177
|
+
|| event.hasPayloadData);
|
|
178
|
+
}
|
|
173
179
|
function hydrateTextForAgent(event, rawData) {
|
|
174
180
|
if (event.kind !== "message") {
|
|
175
181
|
return { ...event, requiresRepair: false };
|
|
@@ -323,6 +329,40 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
323
329
|
throw new Error(`BlueBubbles read failed (${response.status}): ${errorText || "unknown"}`);
|
|
324
330
|
}
|
|
325
331
|
},
|
|
332
|
+
async checkHealth() {
|
|
333
|
+
const url = buildBlueBubblesApiUrl(config.serverUrl, "/api/v1/message/count", config.password);
|
|
334
|
+
(0, runtime_1.emitNervesEvent)({
|
|
335
|
+
component: "senses",
|
|
336
|
+
event: "senses.bluebubbles_healthcheck_start",
|
|
337
|
+
message: "probing bluebubbles upstream health",
|
|
338
|
+
meta: { serverUrl: config.serverUrl },
|
|
339
|
+
});
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: "GET",
|
|
342
|
+
signal: AbortSignal.timeout(channelConfig.requestTimeoutMs),
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
const errorText = await response.text().catch(() => "");
|
|
346
|
+
(0, runtime_1.emitNervesEvent)({
|
|
347
|
+
level: "warn",
|
|
348
|
+
component: "senses",
|
|
349
|
+
event: "senses.bluebubbles_healthcheck_error",
|
|
350
|
+
message: "bluebubbles upstream health probe failed",
|
|
351
|
+
meta: {
|
|
352
|
+
serverUrl: config.serverUrl,
|
|
353
|
+
status: response.status,
|
|
354
|
+
reason: errorText || "unknown",
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
throw new Error(`BlueBubbles upstream health check failed (${response.status}): ${errorText || "unknown"}`);
|
|
358
|
+
}
|
|
359
|
+
(0, runtime_1.emitNervesEvent)({
|
|
360
|
+
component: "senses",
|
|
361
|
+
event: "senses.bluebubbles_healthcheck_end",
|
|
362
|
+
message: "bluebubbles upstream health probe succeeded",
|
|
363
|
+
meta: { serverUrl: config.serverUrl },
|
|
364
|
+
});
|
|
365
|
+
},
|
|
326
366
|
async repairEvent(event) {
|
|
327
367
|
if (!event.requiresRepair) {
|
|
328
368
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -387,7 +427,16 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
387
427
|
type: event.eventType,
|
|
388
428
|
data,
|
|
389
429
|
});
|
|
390
|
-
|
|
430
|
+
const recoveredMessage = event.kind === "mutation"
|
|
431
|
+
&& !event.shouldNotifyAgent
|
|
432
|
+
? (0, bluebubbles_model_1.normalizeBlueBubblesEvent)({
|
|
433
|
+
type: "new-message",
|
|
434
|
+
data,
|
|
435
|
+
})
|
|
436
|
+
: null;
|
|
437
|
+
let hydrated = recoveredMessage && hasRecoverableMessageContent(recoveredMessage)
|
|
438
|
+
? hydrateTextForAgent(recoveredMessage, data)
|
|
439
|
+
: hydrateTextForAgent(normalized, data);
|
|
391
440
|
if (hydrated.kind === "message" &&
|
|
392
441
|
hydrated.balloonBundleId !== "com.apple.messages.URLBalloonProvider" &&
|
|
393
442
|
hydrated.attachments.length > 0) {
|
|
@@ -411,6 +460,7 @@ function createBlueBubblesClient(config = (0, config_1.getBlueBubblesConfig)(),
|
|
|
411
460
|
kind: hydrated.kind,
|
|
412
461
|
messageGuid: hydrated.messageGuid,
|
|
413
462
|
repairedFrom: event.kind,
|
|
463
|
+
promotedFromMutation: event.kind === "mutation" && hydrated.kind === "message",
|
|
414
464
|
},
|
|
415
465
|
});
|
|
416
466
|
return hydrated;
|
|
@@ -8,4 +8,6 @@ if (!process.argv.includes("--agent")) {
|
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
const bluebubbles_1 = require("./bluebubbles");
|
|
11
|
+
const runtime_logging_1 = require("../heart/daemon/runtime-logging");
|
|
12
|
+
(0, runtime_logging_1.configureDaemonRuntimeLogger)("bluebubbles");
|
|
11
13
|
(0, bluebubbles_1.startBlueBubblesApp)();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getBlueBubblesInboundLogPath = getBlueBubblesInboundLogPath;
|
|
37
|
+
exports.hasRecordedBlueBubblesInbound = hasRecordedBlueBubblesInbound;
|
|
38
|
+
exports.recordBlueBubblesInbound = recordBlueBubblesInbound;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const config_1 = require("../heart/config");
|
|
42
|
+
const identity_1 = require("../heart/identity");
|
|
43
|
+
const runtime_1 = require("../nerves/runtime");
|
|
44
|
+
function getBlueBubblesInboundLogPath(agentName, sessionKey) {
|
|
45
|
+
return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "inbound", `${(0, config_1.sanitizeKey)(sessionKey)}.ndjson`);
|
|
46
|
+
}
|
|
47
|
+
function readEntries(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
50
|
+
return raw
|
|
51
|
+
.split("\n")
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.map((line) => JSON.parse(line))
|
|
55
|
+
.filter((entry) => typeof entry.messageGuid === "string" && typeof entry.sessionKey === "string");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function hasRecordedBlueBubblesInbound(agentName, sessionKey, messageGuid) {
|
|
62
|
+
if (!messageGuid.trim())
|
|
63
|
+
return false;
|
|
64
|
+
const filePath = getBlueBubblesInboundLogPath(agentName, sessionKey);
|
|
65
|
+
return readEntries(filePath).some((entry) => entry.messageGuid === messageGuid);
|
|
66
|
+
}
|
|
67
|
+
function recordBlueBubblesInbound(agentName, event, source) {
|
|
68
|
+
const filePath = getBlueBubblesInboundLogPath(agentName, event.chat.sessionKey);
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
fs.appendFileSync(filePath, JSON.stringify({
|
|
72
|
+
recordedAt: new Date(event.timestamp).toISOString(),
|
|
73
|
+
messageGuid: event.messageGuid,
|
|
74
|
+
chatGuid: event.chat.chatGuid ?? null,
|
|
75
|
+
chatIdentifier: event.chat.chatIdentifier ?? null,
|
|
76
|
+
sessionKey: event.chat.sessionKey,
|
|
77
|
+
textForAgent: event.textForAgent,
|
|
78
|
+
source,
|
|
79
|
+
}) + "\n", "utf-8");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
(0, runtime_1.emitNervesEvent)({
|
|
83
|
+
level: "warn",
|
|
84
|
+
component: "senses",
|
|
85
|
+
event: "senses.bluebubbles_inbound_log_error",
|
|
86
|
+
message: "failed to record bluebubbles inbound sidecar log",
|
|
87
|
+
meta: {
|
|
88
|
+
agentName,
|
|
89
|
+
messageGuid: event.messageGuid,
|
|
90
|
+
sessionKey: event.chat.sessionKey,
|
|
91
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return filePath;
|
|
95
|
+
}
|
|
96
|
+
(0, runtime_1.emitNervesEvent)({
|
|
97
|
+
component: "senses",
|
|
98
|
+
event: "senses.bluebubbles_inbound_logged",
|
|
99
|
+
message: "recorded bluebubbles inbound message to sidecar log",
|
|
100
|
+
meta: {
|
|
101
|
+
agentName,
|
|
102
|
+
messageGuid: event.messageGuid,
|
|
103
|
+
sessionKey: event.chat.sessionKey,
|
|
104
|
+
source,
|
|
105
|
+
path: filePath,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return filePath;
|
|
109
|
+
}
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getBlueBubblesMutationLogPath = getBlueBubblesMutationLogPath;
|
|
37
37
|
exports.recordBlueBubblesMutation = recordBlueBubblesMutation;
|
|
38
|
+
exports.listBlueBubblesRecoveryCandidates = listBlueBubblesRecoveryCandidates;
|
|
38
39
|
const fs = __importStar(require("node:fs"));
|
|
39
40
|
const path = __importStar(require("node:path"));
|
|
40
41
|
const runtime_1 = require("../nerves/runtime");
|
|
@@ -72,3 +73,44 @@ function recordBlueBubblesMutation(agentName, event) {
|
|
|
72
73
|
});
|
|
73
74
|
return filePath;
|
|
74
75
|
}
|
|
76
|
+
function listBlueBubblesRecoveryCandidates(agentName) {
|
|
77
|
+
const rootDir = path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "bluebubbles", "mutations");
|
|
78
|
+
let files;
|
|
79
|
+
try {
|
|
80
|
+
files = fs.readdirSync(rootDir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const deduped = new Map();
|
|
86
|
+
for (const file of files.filter((entry) => entry.endsWith(".ndjson")).sort()) {
|
|
87
|
+
const filePath = path.join(rootDir, file);
|
|
88
|
+
let raw = "";
|
|
89
|
+
try {
|
|
90
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const line of raw.split("\n")) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const entry = JSON.parse(trimmed);
|
|
101
|
+
if (typeof entry.messageGuid !== "string"
|
|
102
|
+
|| !entry.messageGuid.trim()
|
|
103
|
+
|| entry.fromMe
|
|
104
|
+
|| entry.shouldNotifyAgent
|
|
105
|
+
|| (entry.mutationType !== "read" && entry.mutationType !== "delivery")) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
deduped.set(entry.messageGuid, entry);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// ignore malformed recovery candidates
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...deduped.values()].sort((left, right) => left.recordedAt.localeCompare(right.recordedAt));
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getBlueBubblesRuntimeStatePath = getBlueBubblesRuntimeStatePath;
|
|
37
|
+
exports.readBlueBubblesRuntimeState = readBlueBubblesRuntimeState;
|
|
38
|
+
exports.writeBlueBubblesRuntimeState = writeBlueBubblesRuntimeState;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const identity_1 = require("../heart/identity");
|
|
42
|
+
const runtime_1 = require("../nerves/runtime");
|
|
43
|
+
const DEFAULT_RUNTIME_STATE = {
|
|
44
|
+
upstreamStatus: "unknown",
|
|
45
|
+
detail: "startup health probe pending",
|
|
46
|
+
pendingRecoveryCount: 0,
|
|
47
|
+
};
|
|
48
|
+
function getBlueBubblesRuntimeStatePath(agentName, agentRoot = (0, identity_1.getAgentRoot)(agentName)) {
|
|
49
|
+
return path.join(agentRoot, "state", "senses", "bluebubbles", "runtime.json");
|
|
50
|
+
}
|
|
51
|
+
function readBlueBubblesRuntimeState(agentName, agentRoot) {
|
|
52
|
+
const filePath = getBlueBubblesRuntimeStatePath(agentName, agentRoot);
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
return {
|
|
57
|
+
upstreamStatus: parsed.upstreamStatus === "ok" || parsed.upstreamStatus === "error"
|
|
58
|
+
? parsed.upstreamStatus
|
|
59
|
+
: "unknown",
|
|
60
|
+
detail: typeof parsed.detail === "string" && parsed.detail.trim()
|
|
61
|
+
? parsed.detail
|
|
62
|
+
: DEFAULT_RUNTIME_STATE.detail,
|
|
63
|
+
lastCheckedAt: typeof parsed.lastCheckedAt === "string" ? parsed.lastCheckedAt : undefined,
|
|
64
|
+
pendingRecoveryCount: typeof parsed.pendingRecoveryCount === "number" && Number.isFinite(parsed.pendingRecoveryCount)
|
|
65
|
+
? parsed.pendingRecoveryCount
|
|
66
|
+
: 0,
|
|
67
|
+
lastRecoveredAt: typeof parsed.lastRecoveredAt === "string" ? parsed.lastRecoveredAt : undefined,
|
|
68
|
+
lastRecoveredMessageGuid: typeof parsed.lastRecoveredMessageGuid === "string"
|
|
69
|
+
? parsed.lastRecoveredMessageGuid
|
|
70
|
+
: undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { ...DEFAULT_RUNTIME_STATE };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writeBlueBubblesRuntimeState(agentName, state, agentRoot) {
|
|
78
|
+
const filePath = getBlueBubblesRuntimeStatePath(agentName, agentRoot);
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
81
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
(0, runtime_1.emitNervesEvent)({
|
|
85
|
+
level: "warn",
|
|
86
|
+
component: "senses",
|
|
87
|
+
event: "senses.bluebubbles_runtime_state_error",
|
|
88
|
+
message: "failed to write bluebubbles runtime state",
|
|
89
|
+
meta: {
|
|
90
|
+
agentName,
|
|
91
|
+
upstreamStatus: state.upstreamStatus,
|
|
92
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return filePath;
|
|
96
|
+
}
|
|
97
|
+
(0, runtime_1.emitNervesEvent)({
|
|
98
|
+
component: "senses",
|
|
99
|
+
event: "senses.bluebubbles_runtime_state_written",
|
|
100
|
+
message: "wrote bluebubbles runtime state",
|
|
101
|
+
meta: {
|
|
102
|
+
agentName,
|
|
103
|
+
upstreamStatus: state.upstreamStatus,
|
|
104
|
+
pendingRecoveryCount: state.pendingRecoveryCount,
|
|
105
|
+
path: filePath,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return filePath;
|
|
109
|
+
}
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.handleBlueBubblesEvent = handleBlueBubblesEvent;
|
|
37
|
+
exports.recoverMissedBlueBubblesMessages = recoverMissedBlueBubblesMessages;
|
|
37
38
|
exports.createBlueBubblesWebhookHandler = createBlueBubblesWebhookHandler;
|
|
38
39
|
exports.drainAndSendPendingBlueBubbles = drainAndSendPendingBlueBubbles;
|
|
39
40
|
exports.startBlueBubblesApp = startBlueBubblesApp;
|
|
@@ -55,7 +56,9 @@ const phrases_1 = require("../mind/phrases");
|
|
|
55
56
|
const runtime_1 = require("../nerves/runtime");
|
|
56
57
|
const bluebubbles_model_1 = require("./bluebubbles-model");
|
|
57
58
|
const bluebubbles_client_1 = require("./bluebubbles-client");
|
|
59
|
+
const bluebubbles_inbound_log_1 = require("./bluebubbles-inbound-log");
|
|
58
60
|
const bluebubbles_mutation_log_1 = require("./bluebubbles-mutation-log");
|
|
61
|
+
const bluebubbles_runtime_state_1 = require("./bluebubbles-runtime-state");
|
|
59
62
|
const bluebubbles_session_cleanup_1 = require("./bluebubbles-session-cleanup");
|
|
60
63
|
const debug_activity_1 = require("./debug-activity");
|
|
61
64
|
const trust_gate_1 = require("./trust-gate");
|
|
@@ -74,6 +77,7 @@ const defaultDeps = {
|
|
|
74
77
|
createFriendResolver: (store, params) => new resolver_1.FriendResolver(store, params),
|
|
75
78
|
createServer: http.createServer,
|
|
76
79
|
};
|
|
80
|
+
const BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS = 30_000;
|
|
77
81
|
function resolveFriendParams(event) {
|
|
78
82
|
if (event.chat.isGroup) {
|
|
79
83
|
const groupKey = event.chat.chatGuid ?? event.chat.chatIdentifier ?? event.sender.externalId;
|
|
@@ -234,6 +238,47 @@ function buildInboundContent(event, existingMessages) {
|
|
|
234
238
|
...event.inputPartsForAgent,
|
|
235
239
|
];
|
|
236
240
|
}
|
|
241
|
+
function sessionLikelyContainsMessage(event, existingMessages) {
|
|
242
|
+
const fragment = event.textForAgent.trim();
|
|
243
|
+
if (!fragment)
|
|
244
|
+
return false;
|
|
245
|
+
return existingMessages.some((message) => {
|
|
246
|
+
if (message.role !== "user")
|
|
247
|
+
return false;
|
|
248
|
+
return extractMessageText(message.content).includes(fragment);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function mutationEntryToEvent(entry) {
|
|
252
|
+
return {
|
|
253
|
+
kind: "mutation",
|
|
254
|
+
eventType: entry.eventType,
|
|
255
|
+
mutationType: entry.mutationType,
|
|
256
|
+
messageGuid: entry.messageGuid,
|
|
257
|
+
targetMessageGuid: entry.targetMessageGuid ?? undefined,
|
|
258
|
+
timestamp: Date.parse(entry.recordedAt) || Date.now(),
|
|
259
|
+
fromMe: entry.fromMe,
|
|
260
|
+
sender: {
|
|
261
|
+
provider: "imessage-handle",
|
|
262
|
+
externalId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
263
|
+
rawId: entry.chatIdentifier ?? entry.chatGuid ?? "unknown",
|
|
264
|
+
displayName: entry.chatIdentifier ?? entry.chatGuid ?? "Unknown",
|
|
265
|
+
},
|
|
266
|
+
chat: {
|
|
267
|
+
chatGuid: entry.chatGuid ?? undefined,
|
|
268
|
+
chatIdentifier: entry.chatIdentifier ?? undefined,
|
|
269
|
+
displayName: undefined,
|
|
270
|
+
isGroup: Boolean(entry.chatGuid?.includes(";+;")),
|
|
271
|
+
sessionKey: entry.sessionKey,
|
|
272
|
+
sendTarget: entry.chatGuid
|
|
273
|
+
? { kind: "chat_guid", value: entry.chatGuid }
|
|
274
|
+
: { kind: "chat_identifier", value: entry.chatIdentifier ?? "unknown" },
|
|
275
|
+
participantHandles: [],
|
|
276
|
+
},
|
|
277
|
+
shouldNotifyAgent: entry.shouldNotifyAgent,
|
|
278
|
+
textForAgent: entry.textForAgent,
|
|
279
|
+
requiresRepair: true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
237
282
|
function getBlueBubblesContinuityIngressTexts(event) {
|
|
238
283
|
if (event.kind !== "message")
|
|
239
284
|
return [];
|
|
@@ -435,10 +480,8 @@ function isWebhookPasswordValid(url, expectedPassword) {
|
|
|
435
480
|
const provided = url.searchParams.get("password");
|
|
436
481
|
return !provided || provided === expectedPassword;
|
|
437
482
|
}
|
|
438
|
-
async function
|
|
439
|
-
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
483
|
+
async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
|
|
440
484
|
const client = resolvedDeps.createClient();
|
|
441
|
-
const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
|
|
442
485
|
if (event.fromMe) {
|
|
443
486
|
(0, runtime_1.emitNervesEvent)({
|
|
444
487
|
component: "senses",
|
|
@@ -509,6 +552,36 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
509
552
|
const sessionMessages = existing?.messages && existing.messages.length > 0
|
|
510
553
|
? existing.messages
|
|
511
554
|
: [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
|
|
555
|
+
if (event.kind === "message") {
|
|
556
|
+
const agentName = resolvedDeps.getAgentName();
|
|
557
|
+
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, event.chat.sessionKey, event.messageGuid)) {
|
|
558
|
+
(0, runtime_1.emitNervesEvent)({
|
|
559
|
+
component: "senses",
|
|
560
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
561
|
+
message: "skipped bluebubbles message already recorded as handled",
|
|
562
|
+
meta: {
|
|
563
|
+
messageGuid: event.messageGuid,
|
|
564
|
+
sessionKey: event.chat.sessionKey,
|
|
565
|
+
source,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
569
|
+
}
|
|
570
|
+
if (source !== "webhook" && sessionLikelyContainsMessage(event, existing?.messages ?? sessionMessages)) {
|
|
571
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(agentName, event, "recovery-bootstrap");
|
|
572
|
+
(0, runtime_1.emitNervesEvent)({
|
|
573
|
+
component: "senses",
|
|
574
|
+
event: "senses.bluebubbles_recovery_skip",
|
|
575
|
+
message: "skipped bluebubbles recovery because the session already contains the message text",
|
|
576
|
+
meta: {
|
|
577
|
+
messageGuid: event.messageGuid,
|
|
578
|
+
sessionKey: event.chat.sessionKey,
|
|
579
|
+
source,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
return { handled: true, notifiedAgent: false, kind: event.kind, reason: "already_processed" };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
512
585
|
// Build inbound user message (adapter concern: BB-specific content formatting)
|
|
513
586
|
const userMessage = {
|
|
514
587
|
role: "user",
|
|
@@ -578,6 +651,9 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
578
651
|
text: result.gateResult.autoReply,
|
|
579
652
|
});
|
|
580
653
|
}
|
|
654
|
+
if (event.kind === "message") {
|
|
655
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
656
|
+
}
|
|
581
657
|
return {
|
|
582
658
|
handled: true,
|
|
583
659
|
notifiedAgent: false,
|
|
@@ -586,6 +662,9 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
586
662
|
}
|
|
587
663
|
// Gate allowed — flush the agent's reply
|
|
588
664
|
await callbacks.flush();
|
|
665
|
+
if (event.kind === "message") {
|
|
666
|
+
(0, bluebubbles_inbound_log_1.recordBlueBubblesInbound)(resolvedDeps.getAgentName(), event, source);
|
|
667
|
+
}
|
|
589
668
|
(0, runtime_1.emitNervesEvent)({
|
|
590
669
|
component: "senses",
|
|
591
670
|
event: "senses.bluebubbles_turn_end",
|
|
@@ -606,6 +685,94 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
|
606
685
|
await callbacks.finish();
|
|
607
686
|
}
|
|
608
687
|
}
|
|
688
|
+
async function handleBlueBubblesEvent(payload, deps = {}) {
|
|
689
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
690
|
+
const client = resolvedDeps.createClient();
|
|
691
|
+
const event = await client.repairEvent((0, bluebubbles_model_1.normalizeBlueBubblesEvent)(payload));
|
|
692
|
+
return handleBlueBubblesNormalizedEvent(event, resolvedDeps, "webhook");
|
|
693
|
+
}
|
|
694
|
+
function countPendingRecoveryCandidates(agentName) {
|
|
695
|
+
return (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)
|
|
696
|
+
.filter((entry) => !(0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, entry.sessionKey, entry.messageGuid))
|
|
697
|
+
.length;
|
|
698
|
+
}
|
|
699
|
+
async function syncBlueBubblesRuntime(deps = {}) {
|
|
700
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
701
|
+
const agentName = resolvedDeps.getAgentName();
|
|
702
|
+
const client = resolvedDeps.createClient();
|
|
703
|
+
const checkedAt = new Date().toISOString();
|
|
704
|
+
try {
|
|
705
|
+
await client.checkHealth();
|
|
706
|
+
const recovery = await recoverMissedBlueBubblesMessages(resolvedDeps);
|
|
707
|
+
(0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
708
|
+
upstreamStatus: recovery.pending > 0 || recovery.failed > 0 ? "error" : "ok",
|
|
709
|
+
detail: recovery.failed > 0
|
|
710
|
+
? `recovery failures: ${recovery.failed}`
|
|
711
|
+
: recovery.pending > 0
|
|
712
|
+
? `pending recovery: ${recovery.pending}`
|
|
713
|
+
: "upstream reachable",
|
|
714
|
+
lastCheckedAt: checkedAt,
|
|
715
|
+
pendingRecoveryCount: recovery.pending,
|
|
716
|
+
lastRecoveredAt: recovery.recovered > 0 ? checkedAt : undefined,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
(0, bluebubbles_runtime_state_1.writeBlueBubblesRuntimeState)(agentName, {
|
|
721
|
+
upstreamStatus: "error",
|
|
722
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
723
|
+
lastCheckedAt: checkedAt,
|
|
724
|
+
pendingRecoveryCount: countPendingRecoveryCandidates(agentName),
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function recoverMissedBlueBubblesMessages(deps = {}) {
|
|
729
|
+
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
730
|
+
const agentName = resolvedDeps.getAgentName();
|
|
731
|
+
const client = resolvedDeps.createClient();
|
|
732
|
+
const result = { recovered: 0, skipped: 0, pending: 0, failed: 0 };
|
|
733
|
+
for (const candidate of (0, bluebubbles_mutation_log_1.listBlueBubblesRecoveryCandidates)(agentName)) {
|
|
734
|
+
if ((0, bluebubbles_inbound_log_1.hasRecordedBlueBubblesInbound)(agentName, candidate.sessionKey, candidate.messageGuid)) {
|
|
735
|
+
result.skipped++;
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const repaired = await client.repairEvent(mutationEntryToEvent(candidate));
|
|
740
|
+
if (repaired.kind !== "message") {
|
|
741
|
+
result.pending++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const handled = await handleBlueBubblesNormalizedEvent(repaired, resolvedDeps, "mutation-recovery");
|
|
745
|
+
if (handled.reason === "already_processed") {
|
|
746
|
+
result.skipped++;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
result.recovered++;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
result.failed++;
|
|
754
|
+
(0, runtime_1.emitNervesEvent)({
|
|
755
|
+
level: "warn",
|
|
756
|
+
component: "senses",
|
|
757
|
+
event: "senses.bluebubbles_recovery_error",
|
|
758
|
+
message: "bluebubbles backlog recovery failed",
|
|
759
|
+
meta: {
|
|
760
|
+
messageGuid: candidate.messageGuid,
|
|
761
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (result.recovered > 0 || result.skipped > 0 || result.pending > 0 || result.failed > 0) {
|
|
767
|
+
(0, runtime_1.emitNervesEvent)({
|
|
768
|
+
component: "senses",
|
|
769
|
+
event: "senses.bluebubbles_recovery_complete",
|
|
770
|
+
message: "bluebubbles backlog recovery pass completed",
|
|
771
|
+
meta: { ...result },
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
609
776
|
function createBlueBubblesWebhookHandler(deps = {}) {
|
|
610
777
|
return async (req, res) => {
|
|
611
778
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -842,6 +1009,12 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
842
1009
|
resolvedDeps.createClient();
|
|
843
1010
|
const channelConfig = (0, config_1.getBlueBubblesChannelConfig)();
|
|
844
1011
|
const server = resolvedDeps.createServer(createBlueBubblesWebhookHandler(deps));
|
|
1012
|
+
const runtimeTimer = setInterval(() => {
|
|
1013
|
+
void syncBlueBubblesRuntime(resolvedDeps);
|
|
1014
|
+
}, BLUEBUBBLES_RUNTIME_SYNC_INTERVAL_MS);
|
|
1015
|
+
server.on?.("close", () => {
|
|
1016
|
+
clearInterval(runtimeTimer);
|
|
1017
|
+
});
|
|
845
1018
|
server.listen(channelConfig.port, () => {
|
|
846
1019
|
(0, runtime_1.emitNervesEvent)({
|
|
847
1020
|
component: "channels",
|
|
@@ -850,5 +1023,6 @@ function startBlueBubblesApp(deps = {}) {
|
|
|
850
1023
|
meta: { port: channelConfig.port, webhookPath: channelConfig.webhookPath },
|
|
851
1024
|
});
|
|
852
1025
|
});
|
|
1026
|
+
void syncBlueBubblesRuntime(resolvedDeps);
|
|
853
1027
|
return server;
|
|
854
1028
|
}
|
package/package.json
CHANGED
package/subagents/README.md
CHANGED
|
@@ -36,13 +36,39 @@ cp subagents/*.md ~/.claude/agents/
|
|
|
36
36
|
## Installing For Codex-Style Skills
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
mkdir -p ~/.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
mkdir -p ~/.agents/skills/work-planner ~/.agents/skills/work-doer ~/.agents/skills/work-merger
|
|
40
|
+
|
|
41
|
+
# Hard-link to keep one source of truth
|
|
42
|
+
ln -f "$(pwd)/subagents/work-planner.md" ~/.agents/skills/work-planner/SKILL.md
|
|
43
|
+
ln -f "$(pwd)/subagents/work-doer.md" ~/.agents/skills/work-doer/SKILL.md
|
|
44
|
+
ln -f "$(pwd)/subagents/work-merger.md" ~/.agents/skills/work-merger/SKILL.md
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
**Important:** For Codex/OpenAI skill installs, use the generic `~/.agents/skills` root and use hard links (`ln`, not `ln -s`). Installing the same skill into both `~/.agents/skills` and `~/.codex/skills` can produce duplicate entries in Codex. Symlinked `SKILL.md` files may load but are not advertised reliably by Codex surfaces. Hard-links break when editors save by replacing the file (new inode). After editing any `subagents/*.md` file, re-run the `ln -f` command for that file to restore the link. You can verify with `stat -f '%i'` — both files should share the same inode.
|
|
48
|
+
|
|
49
|
+
Optional UI metadata:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mkdir -p ~/.agents/skills/work-planner/agents ~/.agents/skills/work-doer/agents ~/.agents/skills/work-merger/agents
|
|
53
|
+
cat > ~/.agents/skills/work-planner/agents/openai.yaml << 'EOF'
|
|
54
|
+
interface:
|
|
55
|
+
display_name: "Work Planner"
|
|
56
|
+
short_description: "Create and gate planning/doing task docs"
|
|
57
|
+
default_prompt: "Use $work-planner to create or update a planning doc, then stop at NEEDS_REVIEW."
|
|
58
|
+
EOF
|
|
59
|
+
cat > ~/.agents/skills/work-doer/agents/openai.yaml << 'EOF'
|
|
60
|
+
interface:
|
|
61
|
+
display_name: "Work Doer"
|
|
62
|
+
short_description: "Execute approved doing docs with strict TDD"
|
|
63
|
+
default_prompt: "Use $work-doer to execute an approved doing doc unit by unit."
|
|
64
|
+
EOF
|
|
65
|
+
cat > ~/.agents/skills/work-merger/agents/openai.yaml << 'EOF'
|
|
66
|
+
interface:
|
|
67
|
+
display_name: "Work Merger"
|
|
68
|
+
short_description: "Merge feature branch into main via PR after work-doer completes"
|
|
69
|
+
default_prompt: "Use $work-merger to merge the current feature branch into main."
|
|
70
|
+
EOF
|
|
71
|
+
```
|
|
46
72
|
|
|
47
73
|
## Keeping Local Skill Copies Fresh
|
|
48
74
|
|
|
@@ -51,8 +77,8 @@ After editing any `subagents/*.md` file, resync your local installed copies.
|
|
|
51
77
|
The repo workflow usually checks this with diffs like:
|
|
52
78
|
|
|
53
79
|
```bash
|
|
54
|
-
diff -q ~/.
|
|
55
|
-
diff -q ~/.
|
|
80
|
+
diff -q ~/.agents/skills/work-planner/SKILL.md subagents/work-planner.md
|
|
81
|
+
diff -q ~/.agents/skills/work-doer/SKILL.md subagents/work-doer.md
|
|
56
82
|
```
|
|
57
83
|
|
|
58
84
|
## Restart Behavior
|
package/subagents/work-doer.md
CHANGED
|
@@ -9,13 +9,14 @@ You are a task executor. Read a doing.md file and execute all units sequentially
|
|
|
9
9
|
## On Startup
|
|
10
10
|
|
|
11
11
|
1. **Find task-doc directory**: Read project instructions (for example `AGENTS.md`) to determine where planning/doing docs live for this repo
|
|
12
|
-
2. **
|
|
13
|
-
3.
|
|
14
|
-
4. If
|
|
15
|
-
5.
|
|
16
|
-
6. **
|
|
12
|
+
2. **Confirm worktree**: Run from the dedicated task worktree required by the project. If the current checkout is shared, ambiguous, or not on the task branch, STOP and switch/create the correct worktree first.
|
|
13
|
+
3. **Find doing doc**: Look for `YYYY-MM-DD-HHMM-doing-*.md` in that project-defined task-doc directory
|
|
14
|
+
4. If multiple found, ask which one
|
|
15
|
+
5. If none found, ask user for location
|
|
16
|
+
6. **Check execution_mode**: Read the doing doc's `Execution Mode` field
|
|
17
|
+
7. **Verify artifacts directory exists**: `{task-name}/` next to `{task-name}.md`
|
|
17
18
|
- If missing, create it: `mkdir {task-name}`
|
|
18
|
-
|
|
19
|
+
8. **Detect resume vs fresh start:**
|
|
19
20
|
- Count completed units (✅) vs total units
|
|
20
21
|
- Check git status for uncommitted changes
|
|
21
22
|
|
|
@@ -218,18 +219,19 @@ When all units are `✅`:
|
|
|
218
219
|
2. **Location**: Read and update doing docs in the project-defined task-doc directory, which may live outside the repo
|
|
219
220
|
3. **Artifacts directory**: Use `{task-name}/` for all outputs, logs, data
|
|
220
221
|
4. **Execution mode**: Honor `pending | spawn | direct` from doing doc
|
|
221
|
-
5. **
|
|
222
|
-
6. **
|
|
223
|
-
7. **
|
|
224
|
-
8. **
|
|
225
|
-
9. **
|
|
226
|
-
10. **
|
|
227
|
-
11. **
|
|
228
|
-
12. **
|
|
229
|
-
13. **
|
|
230
|
-
14.
|
|
231
|
-
15.
|
|
232
|
-
16. **
|
|
233
|
-
17. **
|
|
234
|
-
18. **
|
|
222
|
+
5. **Respect the approved structure**: A `READY_FOR_EXECUTION` doing doc should already be ambiguity-clean. Do not rewrite unit structure unless the user changes scope or the doing doc is actually blocked/inaccurate.
|
|
223
|
+
6. **TDD strictly enforced** — tests before implementation, always
|
|
224
|
+
7. **100% coverage** — no exceptions, no exclude attributes
|
|
225
|
+
8. **Atomic commits** — one logical unit per commit, push after each
|
|
226
|
+
9. **Timestamps from git** — `git log -1 --format="%Y-%m-%d %H:%M"`
|
|
227
|
+
10. **Push after each unit phase complete**
|
|
228
|
+
11. **Update doing.md after each unit** — status and progress log
|
|
229
|
+
12. **Spawn sub-agents for fixes** — don't ask, just do it
|
|
230
|
+
13. **Update docs immediately** — when decisions made, commit right away
|
|
231
|
+
14. **Stop on actual blocker** — unclear requirements or need user input
|
|
232
|
+
15. **/compact proactively** — preserve context between units
|
|
233
|
+
16. **No warnings** — treat warnings as errors
|
|
234
|
+
17. **Run full test suite** — before marking unit complete, not just new tests
|
|
235
|
+
18. **Always compile** — run the project's build command after every implementation/refactor unit. Tests passing is necessary but not sufficient.
|
|
236
|
+
19. **Checklist hygiene is mandatory** — keep doing/planning `Completion Criteria` checklists synchronized with verified completion evidence.
|
|
235
237
|
19. **Verify APIs before importing** — before writing `import { Foo } from './bar'`, use `grep` or `read_file` to confirm `Foo` is actually exported from that module. Never assume an export exists — always check the source first. This prevents wasted cycles on "module has no exported member" errors.
|
|
@@ -11,10 +11,11 @@ You are a task planner for coding work. Help the user define scope, then convert
|
|
|
11
11
|
**Determine task doc directory:**
|
|
12
12
|
1. Read project instructions (for example `AGENTS.md`) to find the canonical task-doc location for the current repo
|
|
13
13
|
2. Derive `AGENT` from the current git branch when the project uses agent-scoped task docs
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
16
|
-
5. If the project
|
|
17
|
-
6.
|
|
14
|
+
3. Confirm the task is running from a dedicated task worktree when the project requires parallel agent work; if the checkout is shared or ambiguous, STOP and tell the caller to create/switch to a dedicated worktree first
|
|
15
|
+
4. Set `TASK_DIR` to the project-defined planning/doing directory
|
|
16
|
+
5. If the project-defined parent location exists but `TASK_DIR` does not, create it
|
|
17
|
+
6. If the project does not define a task-doc location, STOP and ask the user or caller where planning/doing docs should live
|
|
18
|
+
7. Do not assume task docs live in the repo root; many projects keep them externally
|
|
18
19
|
|
|
19
20
|
**Check for existing planning docs:**
|
|
20
21
|
1. Look for `YYYY-MM-DD-HHMM-planning-*.md` files in `TASK_DIR`
|
|
@@ -160,7 +161,7 @@ User answers questions → agent updates doc → agent sets status to `NEEDS_REV
|
|
|
160
161
|
|
|
161
162
|
**CRITICAL: Planning doc is KEPT. Conversion creates a NEW doing doc alongside it in `TASK_DIR`.**
|
|
162
163
|
|
|
163
|
-
Run these passes — announce each. **ALL
|
|
164
|
+
Run these passes — announce each. **ALL 5 PASSES ARE MANDATORY. You must run every pass, even if you think nothing changed. Each pass MUST have its own commit (use "no changes needed" in the commit message if the pass found nothing to fix). Do NOT skip or combine passes.**
|
|
164
165
|
|
|
165
166
|
**Pass 1 — First Draft:**
|
|
166
167
|
- Create `YYYY-MM-DD-HHMM-doing-{short-desc}.md` (same timestamp and short-desc as planning)
|
|
@@ -181,7 +182,14 @@ Run these passes — announce each. **ALL 4 PASSES ARE MANDATORY. You must run e
|
|
|
181
182
|
- Update units if reality differs from what was assumed during planning
|
|
182
183
|
- Commit: `git commit -m "docs(doing): validation pass"` (or `"docs(doing): validation pass - no changes needed"` if nothing to fix)
|
|
183
184
|
|
|
184
|
-
**Pass 4 —
|
|
185
|
+
**Pass 4 — Ambiguity:**
|
|
186
|
+
- Remove doer-facing ambiguity before execution starts
|
|
187
|
+
- Tighten units so a `READY_FOR_EXECUTION` doing doc does not require structural rewrites by `work-doer`
|
|
188
|
+
- Resolve fuzzy phrases like "appropriate files", "as needed", or "wherever the bug is" into concrete targets unless the project instructions explicitly require that flexibility
|
|
189
|
+
- If uncertainty remains, keep it in the planning doc's `Open Questions`, set status to `NEEDS_REVIEW`, and STOP instead of shipping an ambiguous doing doc
|
|
190
|
+
- Commit: `git commit -m "docs(doing): ambiguity pass"` (or `"docs(doing): ambiguity pass - no changes needed"` if nothing to fix)
|
|
191
|
+
|
|
192
|
+
**Pass 5 — Quality:**
|
|
185
193
|
- All units have acceptance criteria?
|
|
186
194
|
- No TBD items?
|
|
187
195
|
- Completion criteria testable?
|