@ouro.bot/cli 0.1.0-alpha.482 → 0.1.0-alpha.484
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/active-work.js +46 -0
- package/dist/heart/background-operations.js +27 -3
- package/dist/heart/core.js +15 -0
- package/dist/heart/daemon/cli-exec.js +104 -15
- package/dist/heart/daemon/cli-help.js +10 -4
- package/dist/heart/daemon/cli-parse.js +11 -5
- package/dist/heart/daemon/cli-render.js +1 -1
- package/dist/heart/daemon/thoughts.js +22 -8
- package/dist/heart/mail-import-discovery.js +318 -0
- package/dist/heart/outlook/outlook-http-routes.js +1 -1
- package/dist/heart/outlook/outlook-http-static.js +4 -0
- package/dist/heart/outlook/outlook-http.js +2 -2
- package/dist/heart/outlook/outlook-types.js +1 -1
- package/dist/heart/outlook/outlook-view.js +3 -3
- package/dist/heart/outlook/readers/agent-machine.js +34 -11
- package/dist/heart/outlook/readers/continuity-readers.js +5 -1
- package/dist/heart/outlook/readers/mail.js +3 -3
- package/dist/heart/provider-failover.js +35 -0
- package/dist/heart/session-events.js +91 -5
- package/dist/heart/turn-context.js +11 -0
- package/dist/mind/context.js +1 -1
- package/dist/mind/prompt.js +6 -3
- package/dist/nerves/coverage/file-completeness.js +1 -0
- package/dist/nerves/observation.js +2 -2
- package/dist/outlook-ui/assets/{index-CPfhbn13.js → index-Cm51CY9W.js} +1 -1
- package/dist/outlook-ui/index.html +2 -2
- package/dist/repertoire/tools-mail.js +2 -2
- package/dist/repertoire/tools-session.js +5 -2
- package/dist/senses/cli/ouro-tui.js +1 -1
- package/dist/senses/inner-dialog.js +5 -7
- package/dist/senses/mail.js +149 -18
- package/dist/senses/pipeline.js +70 -7
- package/package.json +1 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<meta name="color-scheme" content="dark" />
|
|
7
|
-
<title>Ouro
|
|
7
|
+
<title>Ouro Mailbox</title>
|
|
8
8
|
<meta name="description" content="The daemon-hosted shared orientation surface for agents alive on this machine." />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-Cm51CY9W.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-BPr5vNuM.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -262,12 +262,12 @@ async function renderEmptyMailResult(input) {
|
|
|
262
262
|
`mail onboarding status: Mailroom is provisioned for ${input.config.mailboxAddress}, but this agent's encrypted store has 0 messages.`,
|
|
263
263
|
...renderSourceGrantStatus(input.config, input.agentId),
|
|
264
264
|
"interpretation: this is not evidence that the human's HEY inbox is empty; Agent Mail has not yet received or imported mail visible to this agent.",
|
|
265
|
-
`agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, ask the human for
|
|
265
|
+
`agent next move: guide setup from docs/agent-mail-setup.md. If HEY mail is needed, ensure the delegated hey alias exists, first try ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --discover so Ouro can find a browser-downloaded export in .playwright-mcp or Downloads. Only ask the human for a file path if discovery cannot find a unique MBOX, then run ouro mail import-mbox --agent ${input.agentId} --owner-email <human-email> --source hey --file <mbox-path>. Verify with mail_recent/mail_search/Ouro Mailbox.`,
|
|
266
266
|
"validation golden paths before claiming setup works:",
|
|
267
267
|
"1. HEY archive to work object: import the human-provided HEY MBOX and use delegated mail to update a real work object, such as travel plans.",
|
|
268
268
|
"2. Native mail and Screener: send and receive agent-native mail, confirm unknown senders enter Screener, get family authorization for allow/discard, verify sender policy, and confirm discarded mail is recoverable.",
|
|
269
269
|
"3. Cross-sense reaction: use a mail-derived update or decision to trigger another configured sense, such as texting the family member on iMessage when BlueBubbles is available.",
|
|
270
|
-
"4. Ouro
|
|
270
|
+
"4. Ouro Mailbox audit: inspect the read-only mailbox UI for imported mail, native inbound, Screener decisions, outbound draft/send records, and mail access logs.",
|
|
271
271
|
"supporting diagnostics are separate evidence inside those paths, not additional paths; never answer a golden-path question with command names, tool names, or status checks.",
|
|
272
272
|
].join("\n");
|
|
273
273
|
}
|
|
@@ -46,13 +46,13 @@ const manager_1 = require("../heart/bridges/manager");
|
|
|
46
46
|
const session_transcript_1 = require("../heart/session-transcript");
|
|
47
47
|
const session_activity_1 = require("../heart/session-activity");
|
|
48
48
|
const active_work_1 = require("../heart/active-work");
|
|
49
|
-
const background_operations_1 = require("../heart/background-operations");
|
|
50
49
|
const coding_1 = require("./coding");
|
|
51
50
|
const tasks_1 = require("./tasks");
|
|
52
51
|
const pending_1 = require("../mind/pending");
|
|
53
52
|
const obligations_1 = require("../arc/obligations");
|
|
54
53
|
const progress_story_1 = require("../heart/progress-story");
|
|
55
54
|
const cross_chat_delivery_1 = require("../heart/cross-chat-delivery");
|
|
55
|
+
const mail_import_discovery_1 = require("../heart/mail-import-discovery");
|
|
56
56
|
const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
|
|
57
57
|
const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
|
|
58
58
|
async function summarizeSessionTailSafely(options) {
|
|
@@ -264,9 +264,12 @@ async function buildToolActiveWorkFrame(ctx) {
|
|
|
264
264
|
&& obligation.origin.channel === currentSession.channel
|
|
265
265
|
&& obligation.origin.key === currentSession.key)?.content ?? null
|
|
266
266
|
: null;
|
|
267
|
-
const backgroundOperations = (0,
|
|
267
|
+
const backgroundOperations = (0, mail_import_discovery_1.listVisibleBackgroundOperations)({
|
|
268
268
|
agentName: (0, identity_1.getAgentName)(),
|
|
269
269
|
agentRoot,
|
|
270
|
+
repoRoot: process.cwd(),
|
|
271
|
+
homeDir: process.env.HOME,
|
|
272
|
+
nowMs: Date.now(),
|
|
270
273
|
limit: 5,
|
|
271
274
|
});
|
|
272
275
|
return (0, active_work_1.buildActiveWorkFrame)({
|
|
@@ -11,7 +11,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
11
11
|
* Only the "live" area (current streaming + spinner + input) re-renders.
|
|
12
12
|
* This avoids the screen-clearing problem that broke the previous Ink attempt.
|
|
13
13
|
*
|
|
14
|
-
* Design language: ouroboros brand palette from ouroboros.bot /
|
|
14
|
+
* Design language: ouroboros brand palette from ouroboros.bot / Mailbox UI.
|
|
15
15
|
* ZERO business logic here — pure rendering from CliStore state.
|
|
16
16
|
*/
|
|
17
17
|
const react_1 = require("react");
|
|
@@ -187,19 +187,17 @@ function deriveResumeCheckpoint(messages) {
|
|
|
187
187
|
const assistantText = contentToText(lastAssistant.content);
|
|
188
188
|
if (!assistantText)
|
|
189
189
|
return "no prior checkpoint recorded";
|
|
190
|
-
const
|
|
190
|
+
const cleanedLines = assistantText
|
|
191
191
|
.split("\n")
|
|
192
|
-
.map((line) => line.trim())
|
|
192
|
+
.map((line) => line.replace(/<\/?think>/gi, "").trim())
|
|
193
|
+
.filter((line) => line.length > 0);
|
|
194
|
+
const explicitCheckpoint = cleanedLines
|
|
193
195
|
.find((line) => /^checkpoint\s*:/i.test(line));
|
|
194
196
|
if (explicitCheckpoint) {
|
|
195
197
|
const parsed = explicitCheckpoint.replace(/^checkpoint\s*:\s*/i, "").trim();
|
|
196
198
|
return parsed || "no prior checkpoint recorded";
|
|
197
199
|
}
|
|
198
|
-
const firstLine =
|
|
199
|
-
.split("\n")
|
|
200
|
-
.map((line) => line.trim())
|
|
201
|
-
.find((line) => line.length > 0);
|
|
202
|
-
/* v8 ignore next -- unreachable: contentToText().trim() guarantees a non-empty line @preserve */
|
|
200
|
+
const firstLine = cleanedLines[0];
|
|
203
201
|
if (!firstLine)
|
|
204
202
|
return "no prior checkpoint recorded";
|
|
205
203
|
if (firstLine.length <= 220)
|
package/dist/senses/mail.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.scanMailImportDiscoveryAttention = scanMailImportDiscoveryAttention;
|
|
36
37
|
exports.startMailSenseApp = startMailSenseApp;
|
|
37
38
|
const fs = __importStar(require("node:fs"));
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
@@ -40,6 +41,9 @@ const runtime_1 = require("../nerves/runtime");
|
|
|
40
41
|
const identity_1 = require("../heart/identity");
|
|
41
42
|
const runtime_credentials_1 = require("../heart/runtime-credentials");
|
|
42
43
|
const pending_1 = require("../mind/pending");
|
|
44
|
+
const socket_client_1 = require("../heart/daemon/socket-client");
|
|
45
|
+
const background_operations_1 = require("../heart/background-operations");
|
|
46
|
+
const mail_import_discovery_1 = require("../heart/mail-import-discovery");
|
|
43
47
|
const attention_1 = require("../mailroom/attention");
|
|
44
48
|
const reader_1 = require("../mailroom/reader");
|
|
45
49
|
const smtp_ingress_1 = require("../mailroom/smtp-ingress");
|
|
@@ -87,6 +91,9 @@ function runtimeStatePath(agentName) {
|
|
|
87
91
|
function attentionStatePath(agentName) {
|
|
88
92
|
return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "mail", "attention.json");
|
|
89
93
|
}
|
|
94
|
+
function importDiscoveryStatePath(agentName) {
|
|
95
|
+
return path.join((0, identity_1.getAgentRoot)(agentName), "state", "senses", "mail", "import-discovery.json");
|
|
96
|
+
}
|
|
90
97
|
function writeRuntimeState(filePath, state) {
|
|
91
98
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
92
99
|
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
@@ -97,6 +104,107 @@ function writeRuntimeState(filePath, state) {
|
|
|
97
104
|
meta: { agentName: state.agentName, status: state.status, lastQueuedCount: state.lastQueuedCount },
|
|
98
105
|
});
|
|
99
106
|
}
|
|
107
|
+
function emptyImportDiscoveryState(updatedAt) {
|
|
108
|
+
return {
|
|
109
|
+
schemaVersion: 1,
|
|
110
|
+
lastNotifiedFingerprint: null,
|
|
111
|
+
updatedAt,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function readImportDiscoveryState(filePath, updatedAt) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
117
|
+
return {
|
|
118
|
+
schemaVersion: 1,
|
|
119
|
+
lastNotifiedFingerprint: typeof parsed.lastNotifiedFingerprint === "string" && parsed.lastNotifiedFingerprint.trim().length > 0
|
|
120
|
+
? parsed.lastNotifiedFingerprint
|
|
121
|
+
: null,
|
|
122
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : updatedAt,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return emptyImportDiscoveryState(updatedAt);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function writeImportDiscoveryState(filePath, state) {
|
|
130
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
function stringArray(value) {
|
|
134
|
+
if (!Array.isArray(value))
|
|
135
|
+
return [];
|
|
136
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
137
|
+
}
|
|
138
|
+
function renderImportDiscoveryContent(candidatePaths) {
|
|
139
|
+
return [
|
|
140
|
+
"[Mail Import Ready]",
|
|
141
|
+
"A local MBOX archive is ready for delegated-mail backfill.",
|
|
142
|
+
"This may live in a worktree-local Playwright sandbox rather than ~/Downloads.",
|
|
143
|
+
"",
|
|
144
|
+
"recent candidates:",
|
|
145
|
+
...candidatePaths.map((candidatePath) => `- ${candidatePath}`),
|
|
146
|
+
"",
|
|
147
|
+
"If this matches an expected mailbox backfill, run `ouro mail import-mbox --discover --owner-email <email> --source hey --agent <agent>` first so Ouro can pick the matching archive or report ambiguity.",
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
async function scanMailImportDiscoveryAttention(input) {
|
|
151
|
+
const nowMs = input.now?.() ?? Date.now();
|
|
152
|
+
const updatedAt = new Date(nowMs).toISOString();
|
|
153
|
+
const statePath = input.statePath ?? importDiscoveryStatePath(input.agentName);
|
|
154
|
+
const pendingDir = input.pendingDir ?? (0, pending_1.getInnerDialogPendingDir)(input.agentName);
|
|
155
|
+
const state = readImportDiscoveryState(statePath, updatedAt);
|
|
156
|
+
const existingOperations = (0, background_operations_1.listBackgroundOperations)({
|
|
157
|
+
agentName: input.agentName,
|
|
158
|
+
agentRoot: (0, identity_1.getAgentRoot)(input.agentName),
|
|
159
|
+
limit: 10,
|
|
160
|
+
});
|
|
161
|
+
const discovered = (0, mail_import_discovery_1.listAmbientMailImportOperations)({
|
|
162
|
+
agentName: input.agentName,
|
|
163
|
+
agentRoot: (0, identity_1.getAgentRoot)(input.agentName),
|
|
164
|
+
existingOperations,
|
|
165
|
+
repoRoot: (0, identity_1.getRepoRoot)(),
|
|
166
|
+
homeDir: process.env.HOME,
|
|
167
|
+
nowMs,
|
|
168
|
+
})[0] ?? null;
|
|
169
|
+
const fingerprint = typeof discovered?.spec?.fingerprint === "string" && discovered.spec.fingerprint.trim().length > 0
|
|
170
|
+
? discovered.spec.fingerprint
|
|
171
|
+
: null;
|
|
172
|
+
const candidatePaths = stringArray(discovered?.spec?.candidatePaths);
|
|
173
|
+
const shouldQueue = Boolean(discovered && fingerprint && fingerprint !== state.lastNotifiedFingerprint);
|
|
174
|
+
if (shouldQueue) {
|
|
175
|
+
(0, pending_1.queuePendingMessage)(pendingDir, {
|
|
176
|
+
from: "mailroom",
|
|
177
|
+
friendId: "self",
|
|
178
|
+
channel: "mail",
|
|
179
|
+
key: "import-ready",
|
|
180
|
+
content: renderImportDiscoveryContent(candidatePaths),
|
|
181
|
+
timestamp: nowMs,
|
|
182
|
+
mode: "reflect",
|
|
183
|
+
});
|
|
184
|
+
await (0, socket_client_1.requestInnerWake)(input.agentName).catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
writeImportDiscoveryState(statePath, {
|
|
187
|
+
schemaVersion: 1,
|
|
188
|
+
lastNotifiedFingerprint: shouldQueue ? fingerprint : state.lastNotifiedFingerprint,
|
|
189
|
+
updatedAt,
|
|
190
|
+
});
|
|
191
|
+
(0, runtime_1.emitNervesEvent)({
|
|
192
|
+
component: "senses",
|
|
193
|
+
event: "senses.mail_import_discovery_scanned",
|
|
194
|
+
message: "mail import discovery scanned",
|
|
195
|
+
meta: {
|
|
196
|
+
agentName: input.agentName,
|
|
197
|
+
queued: shouldQueue,
|
|
198
|
+
candidateCount: candidatePaths.length,
|
|
199
|
+
fingerprint,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
queued: shouldQueue,
|
|
204
|
+
fingerprint,
|
|
205
|
+
candidatePaths,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
100
208
|
function closeServer(server) {
|
|
101
209
|
return new Promise((resolve) => {
|
|
102
210
|
server.close(resolve);
|
|
@@ -137,34 +245,21 @@ async function startMailSenseApp(options) {
|
|
|
137
245
|
const activeHttpPort = () => ingress ? serverPort(ingress.health) : null;
|
|
138
246
|
const runtimePath = runtimeStatePath(options.agentName);
|
|
139
247
|
const attentionPath = attentionStatePath(options.agentName);
|
|
248
|
+
const importDiscoveryPath = importDiscoveryStatePath(options.agentName);
|
|
140
249
|
let lastScanAt = null;
|
|
141
250
|
let lastQueuedCount = 0;
|
|
142
251
|
const scan = async () => {
|
|
252
|
+
const scanStartedAt = new Date(now()).toISOString();
|
|
253
|
+
let queuedCount = 0;
|
|
143
254
|
try {
|
|
144
|
-
const
|
|
145
|
-
const result = await (0, attention_1.scanMailScreenerAttention)({
|
|
255
|
+
const screener = await (0, attention_1.scanMailScreenerAttention)({
|
|
146
256
|
agentName: options.agentName,
|
|
147
257
|
store: resolved.store,
|
|
148
258
|
pendingDir: (0, pending_1.getInnerDialogPendingDir)(options.agentName),
|
|
149
259
|
statePath: attentionPath,
|
|
150
260
|
now,
|
|
151
261
|
});
|
|
152
|
-
|
|
153
|
-
lastQueuedCount = result.queued.length;
|
|
154
|
-
writeRuntimeState(runtimePath, {
|
|
155
|
-
schemaVersion: 1,
|
|
156
|
-
agentName: options.agentName,
|
|
157
|
-
status: "running",
|
|
158
|
-
mailboxAddress: resolved.config.mailboxAddress,
|
|
159
|
-
smtpPort: activeSmtpPort(),
|
|
160
|
-
httpPort: activeHttpPort(),
|
|
161
|
-
host,
|
|
162
|
-
storeKind: resolved.storeKind,
|
|
163
|
-
storeLabel: resolved.storeLabel,
|
|
164
|
-
lastScanAt,
|
|
165
|
-
lastQueuedCount,
|
|
166
|
-
updatedAt: new Date(now()).toISOString(),
|
|
167
|
-
});
|
|
262
|
+
queuedCount += screener.queued.length;
|
|
168
263
|
}
|
|
169
264
|
catch (error) {
|
|
170
265
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -175,6 +270,42 @@ async function startMailSenseApp(options) {
|
|
|
175
270
|
meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) },
|
|
176
271
|
});
|
|
177
272
|
}
|
|
273
|
+
try {
|
|
274
|
+
const importDiscovery = await scanMailImportDiscoveryAttention({
|
|
275
|
+
agentName: options.agentName,
|
|
276
|
+
pendingDir: (0, pending_1.getInnerDialogPendingDir)(options.agentName),
|
|
277
|
+
statePath: importDiscoveryPath,
|
|
278
|
+
now,
|
|
279
|
+
});
|
|
280
|
+
if (importDiscovery.queued) {
|
|
281
|
+
queuedCount += 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
(0, runtime_1.emitNervesEvent)({
|
|
286
|
+
level: "error",
|
|
287
|
+
component: "senses",
|
|
288
|
+
event: "senses.mail_import_discovery_scan_error",
|
|
289
|
+
message: "mail import discovery scan failed",
|
|
290
|
+
meta: { agentName: options.agentName, error: error instanceof Error ? error.message : String(error) },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
lastScanAt = scanStartedAt;
|
|
294
|
+
lastQueuedCount = queuedCount;
|
|
295
|
+
writeRuntimeState(runtimePath, {
|
|
296
|
+
schemaVersion: 1,
|
|
297
|
+
agentName: options.agentName,
|
|
298
|
+
status: "running",
|
|
299
|
+
mailboxAddress: resolved.config.mailboxAddress,
|
|
300
|
+
smtpPort: activeSmtpPort(),
|
|
301
|
+
httpPort: activeHttpPort(),
|
|
302
|
+
host,
|
|
303
|
+
storeKind: resolved.storeKind,
|
|
304
|
+
storeLabel: resolved.storeLabel,
|
|
305
|
+
lastScanAt,
|
|
306
|
+
lastQueuedCount,
|
|
307
|
+
updatedAt: new Date(now()).toISOString(),
|
|
308
|
+
});
|
|
178
309
|
};
|
|
179
310
|
await scan();
|
|
180
311
|
const intervalMs = Math.max(5_000, resolved.config.attentionIntervalMs ?? 30_000);
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -97,7 +97,23 @@ function resolveCurrentFailoverBinding(agentName, lane) {
|
|
|
97
97
|
const fallback = lane === "inner" ? agentConfig.agentFacing : agentConfig.humanFacing;
|
|
98
98
|
return { provider: fallback.provider, model: fallback.model };
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Apply an agent-driven failover switch to provider state, but only after
|
|
102
|
+
* re-pinging the candidate. The inventory ping that produced the candidate
|
|
103
|
+
* may be stale by the time the agent replies — without this preflight, a
|
|
104
|
+
* "switch to <provider>" reply can move the lane onto an unreachable provider.
|
|
105
|
+
*
|
|
106
|
+
* Returns:
|
|
107
|
+
* { ok: true } — preflight passed, state mutated
|
|
108
|
+
* { ok: false, refused } — preflight failed, state untouched, caller should
|
|
109
|
+
* surface the refusal to the agent
|
|
110
|
+
* Throws on disk errors only (caught by caller as before).
|
|
111
|
+
*/
|
|
112
|
+
async function writeFailoverProviderStateSwitch(agentName, action) {
|
|
113
|
+
const validation = await (0, provider_failover_1.validateFailoverSwitchCandidate)(agentName, { provider: action.provider, model: action.model });
|
|
114
|
+
if (!validation.ok) {
|
|
115
|
+
return { ok: false, refused: true, classification: validation.classification, message: validation.message };
|
|
116
|
+
}
|
|
101
117
|
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
102
118
|
const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
|
|
103
119
|
if (!stateResult.ok) {
|
|
@@ -125,11 +141,36 @@ function writeFailoverProviderStateSwitch(agentName, action) {
|
|
|
125
141
|
lanes,
|
|
126
142
|
readiness,
|
|
127
143
|
});
|
|
144
|
+
return { ok: true };
|
|
128
145
|
}
|
|
129
146
|
function formatFailoverSwitchLabel(action) {
|
|
130
147
|
const provenance = (0, provider_failover_1.formatCredentialProvenanceLabel)(action);
|
|
131
148
|
return `${action.provider} (${action.model}${provenance ? `; ${provenance}` : ""})`;
|
|
132
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Build the operational refusal context message handed back to the agent when
|
|
152
|
+
* a failover switch is rejected by the preflight ping. Slugger-tested format:
|
|
153
|
+
* lead with the refusal + reason, restate the lane that's still standing, then
|
|
154
|
+
* list remaining ready alternatives so the next turn doesn't have to re-enter
|
|
155
|
+
* discovery mode.
|
|
156
|
+
*/
|
|
157
|
+
function buildFailoverSwitchRefusedMessage(pendingContext, refusedAction, refusal) {
|
|
158
|
+
const refusedLabel = formatFailoverSwitchLabel(refusedAction);
|
|
159
|
+
const remaining = pendingContext.readyProviders.filter((candidate) => candidate.provider !== refusedAction.provider);
|
|
160
|
+
const alternativesLine = remaining.length > 0
|
|
161
|
+
? `available verified alternatives right now: ${remaining.map((c) => `${c.provider} (${c.model})`).join(", ")}.`
|
|
162
|
+
: "no other verified alternatives are ready right now.";
|
|
163
|
+
const nextMove = remaining.length > 0
|
|
164
|
+
? `next move: reply "switch to <provider>" picking one of the alternatives above, or tell the user you cannot continue and why.`
|
|
165
|
+
: `next move: ask the operator to repair credentials for ${refusedAction.provider} (or another provider), or tell the user you cannot continue and why.`;
|
|
166
|
+
return [
|
|
167
|
+
`[provider switch refused: tried to switch ${refusedAction.lane} lane to ${refusedLabel}.`,
|
|
168
|
+
`reason: preflight ping failed (${refusal.classification}: ${refusal.message}).`,
|
|
169
|
+
`current lane unchanged: ${pendingContext.currentProvider} / ${pendingContext.currentModel} on the ${pendingContext.currentLane} lane.`,
|
|
170
|
+
alternativesLine,
|
|
171
|
+
nextMove + "]",
|
|
172
|
+
].join(" ");
|
|
173
|
+
}
|
|
133
174
|
function prependTurnSections(message, sections) {
|
|
134
175
|
/* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
|
|
135
176
|
if (message.role !== "user" || sections.length === 0)
|
|
@@ -176,10 +217,9 @@ async function handleInboundTurn(input) {
|
|
|
176
217
|
const failoverAgentName = pendingContext.agentName;
|
|
177
218
|
input.failoverState.pending = null; // always clear before acting
|
|
178
219
|
if (failoverAction.action === "switch") {
|
|
179
|
-
let
|
|
220
|
+
let switchOutcome = null;
|
|
180
221
|
try {
|
|
181
|
-
writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
|
|
182
|
-
switchSucceeded = true;
|
|
222
|
+
switchOutcome = await writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
|
|
183
223
|
/* v8 ignore start -- defensive: write failure during provider switch @preserve */
|
|
184
224
|
}
|
|
185
225
|
catch (switchError) {
|
|
@@ -192,8 +232,7 @@ async function handleInboundTurn(input) {
|
|
|
192
232
|
});
|
|
193
233
|
}
|
|
194
234
|
/* v8 ignore stop */
|
|
195
|
-
|
|
196
|
-
if (switchSucceeded) {
|
|
235
|
+
if (switchOutcome?.ok) {
|
|
197
236
|
(0, runtime_1.emitNervesEvent)({
|
|
198
237
|
component: "senses",
|
|
199
238
|
event: "senses.failover_switch",
|
|
@@ -217,6 +256,29 @@ async function handleInboundTurn(input) {
|
|
|
217
256
|
}];
|
|
218
257
|
input.switchedProvider = failoverAction.provider;
|
|
219
258
|
}
|
|
259
|
+
else if (switchOutcome && !switchOutcome.ok) {
|
|
260
|
+
// Preflight refused the switch — the candidate provider is not actually
|
|
261
|
+
// reachable right now. Keep the existing lane intact and tell the agent
|
|
262
|
+
// what happened so it can pick something else next turn.
|
|
263
|
+
(0, runtime_1.emitNervesEvent)({
|
|
264
|
+
level: "warn",
|
|
265
|
+
component: "senses",
|
|
266
|
+
event: "senses.failover_switch_refused",
|
|
267
|
+
message: `refused failover switch of ${failoverAction.lane} lane to ${failoverAction.provider}: ${switchOutcome.message}`,
|
|
268
|
+
meta: {
|
|
269
|
+
agentName: failoverAgentName,
|
|
270
|
+
lane: failoverAction.lane,
|
|
271
|
+
provider: failoverAction.provider,
|
|
272
|
+
model: failoverAction.model,
|
|
273
|
+
classification: switchOutcome.classification,
|
|
274
|
+
error: switchOutcome.message,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
input.messages = [{
|
|
278
|
+
role: "user",
|
|
279
|
+
content: buildFailoverSwitchRefusedMessage(pendingContext, failoverAction, switchOutcome),
|
|
280
|
+
}];
|
|
281
|
+
}
|
|
220
282
|
// Switch failed OR succeeded — either way, fall through to normal processing.
|
|
221
283
|
}
|
|
222
284
|
}
|
|
@@ -357,7 +419,7 @@ async function handleInboundTurn(input) {
|
|
|
357
419
|
});
|
|
358
420
|
// Propagate sync failure from pre-turn pull
|
|
359
421
|
ctx.syncFailure = syncFailure;
|
|
360
|
-
const { activeBridges, sessionActivity, pendingObligations, codingSessions, otherCodingSessions } = ctx;
|
|
422
|
+
const { activeBridges, sessionActivity, pendingObligations, codingSessions, otherCodingSessions, backgroundOperations } = ctx;
|
|
361
423
|
const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
|
|
362
424
|
const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
|
|
363
425
|
currentSession,
|
|
@@ -366,6 +428,7 @@ async function handleInboundTurn(input) {
|
|
|
366
428
|
inner: ctx.innerWorkState,
|
|
367
429
|
bridges: activeBridges,
|
|
368
430
|
codingSessions,
|
|
431
|
+
backgroundOperations,
|
|
369
432
|
otherCodingSessions,
|
|
370
433
|
pendingObligations,
|
|
371
434
|
taskBoard: ctx.taskBoard,
|