@ouro.bot/cli 0.1.0-alpha.242 → 0.1.0-alpha.244
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/arc/episodes.js +0 -2
- package/dist/arc/json-store.js +0 -3
- package/dist/heart/daemon/agent-discovery.js +34 -0
- package/dist/heart/daemon/cli-render.js +46 -10
- package/dist/heart/daemon/daemon.js +3 -4
- package/dist/heart/sync.js +38 -55
- package/dist/mind/diary.js +0 -5
- package/dist/senses/cli/ouro-tui.js +34 -12
- package/dist/senses/cli/tui-store.js +24 -0
- package/dist/senses/cli.js +11 -1
- package/dist/senses/pipeline.js +408 -415
- package/package.json +1 -1
- package/dist/senses/cli/adapter.js +0 -274
package/dist/senses/pipeline.js
CHANGED
|
@@ -106,454 +106,447 @@ function prependTurnSections(message, sections) {
|
|
|
106
106
|
// ── Pipeline ──────────────────────────────────────────────────────
|
|
107
107
|
let _lastSessionKey = null;
|
|
108
108
|
async function handleInboundTurn(input) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
/* v8 ignore start -- defensive: write failure during provider switch @preserve */
|
|
138
|
-
}
|
|
139
|
-
catch (switchError) {
|
|
140
|
-
(0, runtime_1.emitNervesEvent)({
|
|
141
|
-
level: "error",
|
|
142
|
-
component: "senses",
|
|
143
|
-
event: "senses.failover_switch_error",
|
|
144
|
-
message: `failed to switch provider to ${failoverAction.provider}`,
|
|
145
|
-
meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
/* v8 ignore stop */
|
|
149
|
-
/* v8 ignore next -- false branch: write-failure fallthrough @preserve */
|
|
150
|
-
if (switchSucceeded) {
|
|
151
|
-
(0, runtime_1.emitNervesEvent)({
|
|
152
|
-
component: "senses",
|
|
153
|
-
event: "senses.failover_switch",
|
|
154
|
-
message: `switched provider to ${failoverAction.provider} via failover`,
|
|
155
|
-
meta: { agentName: failoverAgentName, provider: failoverAction.provider },
|
|
156
|
-
});
|
|
157
|
-
// Replace "switch to <provider>" with a context message for the agent.
|
|
158
|
-
// The session already has the user's original question from the failed turn.
|
|
159
|
-
// The agent needs to know what happened so it can respond appropriately.
|
|
160
|
-
const newProviderSecrets = (() => {
|
|
161
|
-
try {
|
|
162
|
-
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
|
|
163
|
-
const cfg = secrets.providers[failoverAction.provider];
|
|
164
|
-
return cfg?.model ?? cfg?.modelName ?? "";
|
|
165
|
-
/* v8 ignore next 2 -- defensive: secrets read failure @preserve */
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return "";
|
|
169
|
-
}
|
|
170
|
-
})();
|
|
171
|
-
const newProviderLabel = newProviderSecrets ? `${failoverAction.provider} (${newProviderSecrets})` : failoverAction.provider;
|
|
172
|
-
input.messages = [{
|
|
173
|
-
role: "user",
|
|
174
|
-
content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
|
|
175
|
-
}];
|
|
176
|
-
input.switchedProvider = failoverAction.provider;
|
|
177
|
-
}
|
|
178
|
-
// Switch failed OR succeeded — either way, fall through to normal processing.
|
|
109
|
+
// Reset session-scoped state when the session changes
|
|
110
|
+
const sessionKey = `${input.channel}/${input.sessionKey ?? "session"}`;
|
|
111
|
+
if (sessionKey !== _lastSessionKey) {
|
|
112
|
+
_lastSessionKey = sessionKey;
|
|
113
|
+
// Reset file-state cache and scrutiny tracking for the new session
|
|
114
|
+
const { fileStateCache } = await Promise.resolve().then(() => __importStar(require("../mind/file-state")));
|
|
115
|
+
const { resetSessionModifiedFiles } = await Promise.resolve().then(() => __importStar(require("../mind/scrutiny")));
|
|
116
|
+
fileStateCache.clear();
|
|
117
|
+
resetSessionModifiedFiles();
|
|
118
|
+
}
|
|
119
|
+
// Step 0: Check for pending failover reply
|
|
120
|
+
if (input.failoverState?.pending) {
|
|
121
|
+
const userText = input.messages
|
|
122
|
+
.filter((m) => m.role === "user")
|
|
123
|
+
.map((m) => typeof m.content === "string" ? m.content : /* v8 ignore next -- defensive: multipart content fallback @preserve */ "")
|
|
124
|
+
.join(" ")
|
|
125
|
+
.trim();
|
|
126
|
+
const pendingContext = input.failoverState.pending;
|
|
127
|
+
const failoverAction = (0, provider_failover_1.handleFailoverReply)(userText, pendingContext);
|
|
128
|
+
const failoverAgentName = pendingContext.agentName;
|
|
129
|
+
input.failoverState.pending = null; // always clear before acting
|
|
130
|
+
if (failoverAction.action === "switch") {
|
|
131
|
+
let switchSucceeded = false;
|
|
132
|
+
try {
|
|
133
|
+
(0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "human", failoverAction.provider);
|
|
134
|
+
(0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "agent", failoverAction.provider);
|
|
135
|
+
switchSucceeded = true;
|
|
136
|
+
/* v8 ignore start -- defensive: write failure during provider switch @preserve */
|
|
179
137
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
?
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
138
|
+
catch (switchError) {
|
|
139
|
+
(0, runtime_1.emitNervesEvent)({
|
|
140
|
+
level: "error",
|
|
141
|
+
component: "senses",
|
|
142
|
+
event: "senses.failover_switch_error",
|
|
143
|
+
message: `failed to switch provider to ${failoverAction.provider}`,
|
|
144
|
+
meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/* v8 ignore stop */
|
|
148
|
+
/* v8 ignore next -- false branch: write-failure fallthrough @preserve */
|
|
149
|
+
if (switchSucceeded) {
|
|
150
|
+
(0, runtime_1.emitNervesEvent)({
|
|
151
|
+
component: "senses",
|
|
152
|
+
event: "senses.failover_switch",
|
|
153
|
+
message: `switched provider to ${failoverAction.provider} via failover`,
|
|
154
|
+
meta: { agentName: failoverAgentName, provider: failoverAction.provider },
|
|
155
|
+
});
|
|
156
|
+
// Replace "switch to <provider>" with a context message for the agent.
|
|
157
|
+
// The session already has the user's original question from the failed turn.
|
|
158
|
+
// The agent needs to know what happened so it can respond appropriately.
|
|
159
|
+
const newProviderSecrets = (() => {
|
|
160
|
+
try {
|
|
161
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
|
|
162
|
+
const cfg = secrets.providers[failoverAction.provider];
|
|
163
|
+
return cfg?.model ?? cfg?.modelName ?? "";
|
|
164
|
+
/* v8 ignore next 2 -- defensive: secrets read failure @preserve */
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return "";
|
|
204
168
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
169
|
+
})();
|
|
170
|
+
const newProviderLabel = newProviderSecrets ? `${failoverAction.provider} (${newProviderSecrets})` : failoverAction.provider;
|
|
171
|
+
input.messages = [{
|
|
172
|
+
role: "user",
|
|
173
|
+
content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
|
|
174
|
+
}];
|
|
175
|
+
input.switchedProvider = failoverAction.provider;
|
|
176
|
+
}
|
|
177
|
+
// Switch failed OR succeeded — either way, fall through to normal processing.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Step 0b: Slash command interception (before friend resolution / agent turn)
|
|
181
|
+
{
|
|
182
|
+
const firstUserMsg = input.messages.find((m) => m.role === "user");
|
|
183
|
+
const userText = firstUserMsg
|
|
184
|
+
? (typeof firstUserMsg.content === "string"
|
|
185
|
+
? firstUserMsg.content
|
|
186
|
+
: Array.isArray(firstUserMsg.content)
|
|
187
|
+
? firstUserMsg.content.find((p) => p.type === "text")?.text ?? ""
|
|
188
|
+
: /* v8 ignore next -- defensive: content is always string or array @preserve */ "")
|
|
189
|
+
: "";
|
|
190
|
+
const parsed = (0, commands_1.parseSlashCommand)(userText);
|
|
191
|
+
if (parsed) {
|
|
192
|
+
const registry = (0, commands_1.getSharedCommandRegistry)();
|
|
193
|
+
const dispatchResult = registry.dispatch(parsed.command, { channel: input.channel });
|
|
194
|
+
if (dispatchResult.handled && dispatchResult.result) {
|
|
195
|
+
(0, runtime_1.emitNervesEvent)({
|
|
196
|
+
component: "senses",
|
|
197
|
+
event: "senses.pipeline_command",
|
|
198
|
+
message: `slash command intercepted: /${parsed.command}`,
|
|
199
|
+
meta: { command: parsed.command, channel: input.channel },
|
|
200
|
+
});
|
|
201
|
+
if (dispatchResult.result.message) {
|
|
202
|
+
input.callbacks.onTextChunk(dispatchResult.result.message);
|
|
213
203
|
}
|
|
204
|
+
// Return a minimal result — no agent turn, no session write
|
|
205
|
+
const resolvedContext = await input.friendResolver.resolve();
|
|
206
|
+
return {
|
|
207
|
+
resolvedContext,
|
|
208
|
+
gateResult: { allowed: true },
|
|
209
|
+
turnOutcome: "command",
|
|
210
|
+
commandAction: dispatchResult.result.action,
|
|
211
|
+
};
|
|
214
212
|
}
|
|
215
213
|
}
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
}
|
|
215
|
+
// Step 1: Resolve friend
|
|
216
|
+
const resolvedContext = await input.friendResolver.resolve();
|
|
217
|
+
(0, runtime_1.emitNervesEvent)({
|
|
218
|
+
component: "senses",
|
|
219
|
+
event: "senses.pipeline_start",
|
|
220
|
+
message: "inbound turn pipeline started",
|
|
221
|
+
meta: {
|
|
222
|
+
channel: input.channel,
|
|
223
|
+
friendId: resolvedContext.friend.id,
|
|
224
|
+
senseType: input.capabilities.senseType,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
// Step 2: Trust gate
|
|
228
|
+
const gateInput = {
|
|
229
|
+
friend: resolvedContext.friend,
|
|
230
|
+
provider: input.provider ?? "local",
|
|
231
|
+
externalId: input.externalId ?? "",
|
|
232
|
+
tenantId: input.tenantId,
|
|
233
|
+
channel: input.channel,
|
|
234
|
+
senseType: input.capabilities.senseType,
|
|
235
|
+
isGroupChat: input.isGroupChat ?? false,
|
|
236
|
+
groupHasFamilyMember: input.groupHasFamilyMember ?? false,
|
|
237
|
+
hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
|
|
238
|
+
};
|
|
239
|
+
const gateResult = input.enforceTrustGate(gateInput);
|
|
240
|
+
// Gate rejection: return early, no agent turn
|
|
241
|
+
if (!gateResult.allowed) {
|
|
218
242
|
(0, runtime_1.emitNervesEvent)({
|
|
219
243
|
component: "senses",
|
|
220
|
-
event: "senses.
|
|
221
|
-
message: "inbound turn
|
|
244
|
+
event: "senses.pipeline_gate_reject",
|
|
245
|
+
message: "trust gate rejected inbound turn",
|
|
222
246
|
meta: {
|
|
223
247
|
channel: input.channel,
|
|
224
248
|
friendId: resolvedContext.friend.id,
|
|
225
|
-
|
|
249
|
+
reason: gateResult.reason,
|
|
226
250
|
},
|
|
227
251
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
provider: input.provider ?? "local",
|
|
232
|
-
externalId: input.externalId ?? "",
|
|
233
|
-
tenantId: input.tenantId,
|
|
234
|
-
channel: input.channel,
|
|
235
|
-
senseType: input.capabilities.senseType,
|
|
236
|
-
isGroupChat: input.isGroupChat ?? false,
|
|
237
|
-
groupHasFamilyMember: input.groupHasFamilyMember ?? false,
|
|
238
|
-
hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
|
|
252
|
+
return {
|
|
253
|
+
resolvedContext,
|
|
254
|
+
gateResult,
|
|
239
255
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
}
|
|
257
|
+
// Step 3: Load/create session
|
|
258
|
+
const session = await input.sessionLoader.loadOrCreate();
|
|
259
|
+
const sessionMessages = session.messages;
|
|
260
|
+
let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
|
|
261
|
+
const lastFriendActivityAt = input.channel === "inner"
|
|
262
|
+
? session.state?.lastFriendActivityAt
|
|
263
|
+
: new Date().toISOString();
|
|
264
|
+
const currentObligation = input.continuityIngressTexts
|
|
265
|
+
?.map((text) => text.trim())
|
|
266
|
+
.filter((text) => text.length > 0)
|
|
267
|
+
.at(-1);
|
|
268
|
+
const currentSession = {
|
|
269
|
+
friendId: resolvedContext.friend.id,
|
|
270
|
+
channel: input.channel,
|
|
271
|
+
key: input.sessionKey ?? "session",
|
|
272
|
+
sessionPath: session.sessionPath,
|
|
273
|
+
};
|
|
274
|
+
// Step 3b: Pre-turn sync pull (opt-in) — MUST happen before any continuity state reads
|
|
275
|
+
// so that obligations, episodes, cares, etc. reflect the latest remote state.
|
|
276
|
+
let syncFailure;
|
|
277
|
+
let syncConfig = { enabled: false, remote: "origin" };
|
|
278
|
+
try {
|
|
279
|
+
syncConfig = (0, config_1.getSyncConfig)();
|
|
280
|
+
}
|
|
281
|
+
catch { /* config not available */ }
|
|
282
|
+
// Wrap the turn body in try/finally so postTurnPush always runs — even on
|
|
283
|
+
// error or early-return failover paths.
|
|
284
|
+
try {
|
|
285
|
+
/* v8 ignore start -- sync-enabled branches tested in sync.test.ts, pipeline tests mock at module boundary @preserve */
|
|
286
|
+
if (syncConfig.enabled) {
|
|
287
|
+
const pullResult = (0, sync_1.preTurnPull)((0, identity_1.getAgentRoot)(), syncConfig);
|
|
288
|
+
if (!pullResult.ok) {
|
|
289
|
+
syncFailure = pullResult.error;
|
|
290
|
+
}
|
|
291
|
+
// Check for pending-sync from a prior failed push
|
|
292
|
+
if (!syncFailure) {
|
|
293
|
+
const pendingSyncPath = path.join((0, identity_1.getAgentRoot)(), "state", "pending-sync.json");
|
|
294
|
+
try {
|
|
295
|
+
if (fs.existsSync(pendingSyncPath)) {
|
|
296
|
+
const pendingSync = JSON.parse(fs.readFileSync(pendingSyncPath, "utf-8"));
|
|
297
|
+
syncFailure = `prior sync push failed: ${pendingSync.error ?? "unknown"}`;
|
|
298
|
+
fs.unlinkSync(pendingSyncPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// Ignore read errors for pending-sync
|
|
303
|
+
}
|
|
304
|
+
}
|
|
257
305
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
const lastFriendActivityAt = input.channel === "inner"
|
|
263
|
-
? session.state?.lastFriendActivityAt
|
|
264
|
-
: new Date().toISOString();
|
|
265
|
-
const currentObligation = input.continuityIngressTexts
|
|
266
|
-
?.map((text) => text.trim())
|
|
267
|
-
.filter((text) => text.length > 0)
|
|
268
|
-
.at(-1);
|
|
269
|
-
const currentSession = {
|
|
270
|
-
friendId: resolvedContext.friend.id,
|
|
306
|
+
/* v8 ignore stop */
|
|
307
|
+
// Build the turn context snapshot — centralizes all state reads
|
|
308
|
+
const ctx = await (0, turn_context_1.buildTurnContext)({
|
|
309
|
+
currentSession,
|
|
271
310
|
channel: input.channel,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
friendStore: input.friendStore,
|
|
312
|
+
});
|
|
313
|
+
// Propagate sync failure from pre-turn pull
|
|
314
|
+
ctx.syncFailure = syncFailure;
|
|
315
|
+
const { activeBridges, sessionActivity, pendingObligations, codingSessions, otherCodingSessions } = ctx;
|
|
316
|
+
const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
|
|
317
|
+
const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
|
|
318
|
+
currentSession,
|
|
319
|
+
currentObligation,
|
|
320
|
+
mustResolveBeforeHandoff,
|
|
321
|
+
inner: ctx.innerWorkState,
|
|
322
|
+
bridges: activeBridges,
|
|
323
|
+
codingSessions,
|
|
324
|
+
otherCodingSessions,
|
|
325
|
+
pendingObligations,
|
|
326
|
+
taskBoard: ctx.taskBoard,
|
|
327
|
+
friendActivity: sessionActivity,
|
|
328
|
+
targetCandidates: ctx.targetCandidates,
|
|
329
|
+
innerReturnObligations: ctx.returnObligations,
|
|
330
|
+
});
|
|
331
|
+
const delegationDecision = (0, delegation_1.decideDelegation)({
|
|
332
|
+
channel: input.channel,
|
|
333
|
+
ingressTexts: input.continuityIngressTexts ?? [],
|
|
334
|
+
activeWork: activeWorkFrame,
|
|
335
|
+
mustResolveBeforeHandoff,
|
|
336
|
+
});
|
|
337
|
+
// Step 4: Drain deferred friend returns, then ordinary per-session pending.
|
|
338
|
+
const deferredReturns = input.channel === "inner"
|
|
339
|
+
? []
|
|
340
|
+
: (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
|
|
341
|
+
const sessionPending = input.drainPending(input.pendingDir);
|
|
342
|
+
const pending = [...deferredReturns, ...sessionPending];
|
|
343
|
+
// Assemble messages: session messages + pending + inbound user messages
|
|
344
|
+
// NOTE: live world-state checkpoint and pending messages are rendered via buildSystem (system prompt sections)
|
|
345
|
+
const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
|
|
346
|
+
// extraPrefixSections from onPendingDrained still prepend to user message (e.g., inner dialog wakes)
|
|
347
|
+
if (extraPrefixSections.length > 0 && input.messages.length > 0) {
|
|
348
|
+
input.messages[0] = prependTurnSections(input.messages[0], extraPrefixSections);
|
|
349
|
+
}
|
|
350
|
+
// Append user messages from the inbound turn
|
|
351
|
+
for (const msg of input.messages) {
|
|
352
|
+
sessionMessages.push(msg);
|
|
281
353
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// the next turn's accumulator.
|
|
354
|
+
// Step 4b: Continuity pipeline — derive tempo, build start-of-turn packet, snapshot obligations
|
|
355
|
+
let renderedStartOfTurnPacket;
|
|
356
|
+
const preTurnObligationIds = new Set(pendingObligations.map((ob) => `${ob.id}:${ob.status}`));
|
|
286
357
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// Ignore read errors for pending-sync
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
/* v8 ignore stop */
|
|
309
|
-
// Build the turn context snapshot — centralizes all state reads
|
|
310
|
-
const ctx = await (0, turn_context_1.buildTurnContext)({
|
|
311
|
-
currentSession,
|
|
312
|
-
channel: input.channel,
|
|
313
|
-
friendStore: input.friendStore,
|
|
358
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
359
|
+
const agentName = (0, identity_1.getAgentName)();
|
|
360
|
+
const { recentEpisodes, activeCares } = ctx;
|
|
361
|
+
const tempoState = (0, tempo_1.deriveTempo)({
|
|
362
|
+
activeSessions: sessionActivity.length + 1,
|
|
363
|
+
openObligations: pendingObligations.length,
|
|
364
|
+
recentEpisodeCount: recentEpisodes.length,
|
|
365
|
+
lastActivityAgeMs: sessionActivity.length > 0
|
|
366
|
+
? Date.now() - new Date(sessionActivity[0].lastActivityAt).getTime()
|
|
367
|
+
: 0,
|
|
368
|
+
hasBlockers: false, // obligations use specific statuses, not "blocked"
|
|
369
|
+
highSalienceEpisodes: recentEpisodes.filter((ep) => ep.salience === "high" || ep.salience === "critical").length,
|
|
370
|
+
activeCareCount: activeCares.length,
|
|
371
|
+
atRiskCareCount: activeCares.filter((c) => c.currentRisk != null).length,
|
|
314
372
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mustResolveBeforeHandoff,
|
|
323
|
-
inner: ctx.innerWorkState,
|
|
324
|
-
bridges: activeBridges,
|
|
325
|
-
codingSessions,
|
|
326
|
-
otherCodingSessions,
|
|
327
|
-
pendingObligations,
|
|
328
|
-
taskBoard: ctx.taskBoard,
|
|
329
|
-
friendActivity: sessionActivity,
|
|
330
|
-
targetCandidates: ctx.targetCandidates,
|
|
331
|
-
innerReturnObligations: ctx.returnObligations,
|
|
373
|
+
const temporalView = (0, temporal_view_1.buildTemporalView)(agentRoot, {
|
|
374
|
+
tempo: tempoState.mode,
|
|
375
|
+
preloaded: {
|
|
376
|
+
recentEpisodes,
|
|
377
|
+
activeObligations: pendingObligations,
|
|
378
|
+
activeCares,
|
|
379
|
+
},
|
|
332
380
|
});
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
381
|
+
const startOfTurnPacket = (0, start_of_turn_packet_1.buildStartOfTurnPacket)(temporalView, {
|
|
382
|
+
canonicalObligations: {
|
|
383
|
+
primary: activeWorkFrame.primaryObligation,
|
|
384
|
+
all: activeWorkFrame.pendingObligations,
|
|
385
|
+
},
|
|
338
386
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
: (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
|
|
343
|
-
const sessionPending = input.drainPending(input.pendingDir);
|
|
344
|
-
const pending = [...deferredReturns, ...sessionPending];
|
|
345
|
-
// Assemble messages: session messages + pending + inbound user messages
|
|
346
|
-
// NOTE: live world-state checkpoint and pending messages are rendered via buildSystem (system prompt sections)
|
|
347
|
-
const extraPrefixSections = input.onPendingDrained?.(pending) ?? [];
|
|
348
|
-
// extraPrefixSections from onPendingDrained still prepend to user message (e.g., inner dialog wakes)
|
|
349
|
-
if (extraPrefixSections.length > 0 && input.messages.length > 0) {
|
|
350
|
-
input.messages[0] = prependTurnSections(input.messages[0], extraPrefixSections);
|
|
387
|
+
/* v8 ignore next 3 -- syncFailure propagation tested in sync.test.ts @preserve */
|
|
388
|
+
if (syncFailure) {
|
|
389
|
+
startOfTurnPacket.syncFailure = syncFailure;
|
|
351
390
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
391
|
+
const capabilities = (0, start_of_turn_packet_1.buildCapabilitiesSection)(agentRoot);
|
|
392
|
+
if (capabilities) {
|
|
393
|
+
startOfTurnPacket.capabilities = capabilities;
|
|
355
394
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
395
|
+
renderedStartOfTurnPacket = (0, start_of_turn_packet_1.renderStartOfTurnPacket)(startOfTurnPacket);
|
|
396
|
+
if (!renderedStartOfTurnPacket)
|
|
397
|
+
renderedStartOfTurnPacket = undefined;
|
|
398
|
+
// Update self-presence
|
|
399
|
+
const presence = (0, presence_1.derivePresence)(agentRoot, agentName, {
|
|
400
|
+
activeSessions: sessionActivity.length + 1,
|
|
401
|
+
openObligations: pendingObligations.length,
|
|
402
|
+
activeBridges: activeBridges.length,
|
|
403
|
+
codingLanes: codingSessions.length,
|
|
404
|
+
currentTempo: tempoState.mode,
|
|
405
|
+
});
|
|
406
|
+
(0, presence_1.writePresence)(agentRoot, agentName, presence);
|
|
407
|
+
}
|
|
408
|
+
catch (continuityError) {
|
|
409
|
+
(0, runtime_1.emitNervesEvent)({
|
|
410
|
+
level: "warn",
|
|
411
|
+
component: "senses",
|
|
412
|
+
event: "senses.continuity_error",
|
|
413
|
+
message: "continuity pipeline failed, continuing without start-of-turn packet",
|
|
414
|
+
meta: { error: continuityError instanceof Error ? continuityError.message : String(continuityError) },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// Step 5: runAgent
|
|
418
|
+
const existingToolContext = input.runAgentOptions?.toolContext;
|
|
419
|
+
const runAgentOptions = {
|
|
420
|
+
...input.runAgentOptions,
|
|
421
|
+
bridgeContext,
|
|
422
|
+
activeWorkFrame,
|
|
423
|
+
delegationDecision,
|
|
424
|
+
startOfTurnPacket: renderedStartOfTurnPacket,
|
|
425
|
+
pendingMessages: pending.length > 0 ? pending.map((msg) => ({ from: msg.from, content: msg.content })) : undefined,
|
|
426
|
+
currentSessionKey: currentSession.key,
|
|
427
|
+
currentObligation,
|
|
428
|
+
mustResolveBeforeHandoff,
|
|
429
|
+
setMustResolveBeforeHandoff: (value) => {
|
|
430
|
+
mustResolveBeforeHandoff = value;
|
|
431
|
+
},
|
|
432
|
+
// Pre-read state from TurnContext for prompt assembly
|
|
433
|
+
daemonRunning: ctx.daemonRunning,
|
|
434
|
+
senseStatusLines: ctx.senseStatusLines,
|
|
435
|
+
bundleMeta: ctx.bundleMeta,
|
|
436
|
+
daemonHealth: ctx.daemonHealth,
|
|
437
|
+
journalFiles: ctx.journalFiles,
|
|
438
|
+
toolContext: {
|
|
439
|
+
/* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
|
|
440
|
+
signin: async () => undefined,
|
|
441
|
+
...existingToolContext,
|
|
442
|
+
context: resolvedContext,
|
|
443
|
+
friendStore: input.friendStore,
|
|
444
|
+
currentSession,
|
|
445
|
+
activeBridges,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
|
|
449
|
+
// Step 5b: Failover on terminal error
|
|
450
|
+
if (result.outcome === "errored" && input.failoverState) {
|
|
359
451
|
try {
|
|
360
|
-
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
361
452
|
const agentName = (0, identity_1.getAgentName)();
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
atRiskCareCount: activeCares.filter((c) => c.currentRisk != null).length,
|
|
374
|
-
});
|
|
375
|
-
const temporalView = (0, temporal_view_1.buildTemporalView)(agentRoot, {
|
|
376
|
-
tempo: tempoState.mode,
|
|
377
|
-
preloaded: {
|
|
378
|
-
recentEpisodes,
|
|
379
|
-
activeObligations: pendingObligations,
|
|
380
|
-
activeCares,
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
const startOfTurnPacket = (0, start_of_turn_packet_1.buildStartOfTurnPacket)(temporalView, {
|
|
384
|
-
canonicalObligations: {
|
|
385
|
-
primary: activeWorkFrame.primaryObligation,
|
|
386
|
-
all: activeWorkFrame.pendingObligations,
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
/* v8 ignore next 3 -- syncFailure propagation tested in sync.test.ts @preserve */
|
|
390
|
-
if (syncFailure) {
|
|
391
|
-
startOfTurnPacket.syncFailure = syncFailure;
|
|
392
|
-
}
|
|
393
|
-
const capabilities = (0, start_of_turn_packet_1.buildCapabilitiesSection)(agentRoot);
|
|
394
|
-
if (capabilities) {
|
|
395
|
-
startOfTurnPacket.capabilities = capabilities;
|
|
453
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
454
|
+
const currentProvider = agentConfig.humanFacing.provider;
|
|
455
|
+
/* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
|
|
456
|
+
const classification = result.errorClassification ?? "unknown";
|
|
457
|
+
const inventory = await (0, provider_ping_1.runHealthInventory)(agentName, currentProvider);
|
|
458
|
+
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
|
|
459
|
+
const providerModels = {};
|
|
460
|
+
for (const [p, cfg] of Object.entries(secrets.providers)) {
|
|
461
|
+
const model = cfg.model ?? cfg.modelName;
|
|
462
|
+
if (typeof model === "string" && model)
|
|
463
|
+
providerModels[p] = model;
|
|
396
464
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
465
|
+
// Use agent.json model (source of truth), not secrets model (may be stale)
|
|
466
|
+
const currentModel = agentConfig.humanFacing.model;
|
|
467
|
+
const failoverContext = (0, provider_failover_1.buildFailoverContext)(
|
|
468
|
+
/* v8 ignore next -- defensive: error always set when errored @preserve */
|
|
469
|
+
result.error?.message ?? "unknown error", classification, currentProvider, currentModel, agentName, inventory, providerModels);
|
|
470
|
+
input.failoverState.pending = failoverContext;
|
|
471
|
+
input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
472
|
+
return {
|
|
473
|
+
resolvedContext,
|
|
474
|
+
gateResult,
|
|
475
|
+
usage: result.usage,
|
|
476
|
+
turnOutcome: result.outcome,
|
|
477
|
+
sessionPath: session.sessionPath,
|
|
478
|
+
messages: sessionMessages,
|
|
479
|
+
drainedPending: pending,
|
|
480
|
+
failoverMessage: failoverContext.userMessage,
|
|
481
|
+
};
|
|
482
|
+
/* v8 ignore start -- failover catch: tested via pipeline failover sequence throws test but v8 under-reports catch coverage @preserve */
|
|
409
483
|
}
|
|
410
|
-
catch (
|
|
484
|
+
catch (failoverError) {
|
|
411
485
|
(0, runtime_1.emitNervesEvent)({
|
|
412
486
|
level: "warn",
|
|
413
487
|
component: "senses",
|
|
414
|
-
event: "senses.
|
|
415
|
-
message: "
|
|
416
|
-
meta: { error:
|
|
488
|
+
event: "senses.failover_error",
|
|
489
|
+
message: "failover sequence failed, falling through",
|
|
490
|
+
meta: { error: failoverError instanceof Error ? failoverError.message : String(failoverError) },
|
|
417
491
|
});
|
|
418
492
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
456
|
-
const currentProvider = agentConfig.humanFacing.provider;
|
|
457
|
-
/* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
|
|
458
|
-
const classification = result.errorClassification ?? "unknown";
|
|
459
|
-
const inventory = await (0, provider_ping_1.runHealthInventory)(agentName, currentProvider);
|
|
460
|
-
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
|
|
461
|
-
const providerModels = {};
|
|
462
|
-
for (const [p, cfg] of Object.entries(secrets.providers)) {
|
|
463
|
-
const model = cfg.model ?? cfg.modelName;
|
|
464
|
-
if (typeof model === "string" && model)
|
|
465
|
-
providerModels[p] = model;
|
|
466
|
-
}
|
|
467
|
-
// Use agent.json model (source of truth), not secrets model (may be stale)
|
|
468
|
-
const currentModel = agentConfig.humanFacing.model;
|
|
469
|
-
const failoverContext = (0, provider_failover_1.buildFailoverContext)(
|
|
470
|
-
/* v8 ignore next -- defensive: error always set when errored @preserve */
|
|
471
|
-
result.error?.message ?? "unknown error", classification, currentProvider, currentModel, agentName, inventory, providerModels);
|
|
472
|
-
input.failoverState.pending = failoverContext;
|
|
473
|
-
input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
474
|
-
return {
|
|
475
|
-
resolvedContext,
|
|
476
|
-
gateResult,
|
|
477
|
-
usage: result.usage,
|
|
478
|
-
turnOutcome: result.outcome,
|
|
479
|
-
sessionPath: session.sessionPath,
|
|
480
|
-
messages: sessionMessages,
|
|
481
|
-
drainedPending: pending,
|
|
482
|
-
failoverMessage: failoverContext.userMessage,
|
|
483
|
-
};
|
|
484
|
-
/* v8 ignore start -- failover catch: tested via pipeline failover sequence throws test but v8 under-reports catch coverage @preserve */
|
|
485
|
-
}
|
|
486
|
-
catch (failoverError) {
|
|
487
|
-
(0, runtime_1.emitNervesEvent)({
|
|
488
|
-
level: "warn",
|
|
489
|
-
component: "senses",
|
|
490
|
-
event: "senses.failover_error",
|
|
491
|
-
message: "failover sequence failed, falling through",
|
|
492
|
-
meta: { error: failoverError instanceof Error ? failoverError.message : String(failoverError) },
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
/* v8 ignore stop */
|
|
496
|
-
}
|
|
497
|
-
// Step 5c: Emit episodes for obligation state transitions
|
|
493
|
+
/* v8 ignore stop */
|
|
494
|
+
}
|
|
495
|
+
// Step 5c: Emit episodes for obligation state transitions
|
|
496
|
+
try {
|
|
497
|
+
const agentRoot = (0, identity_1.getAgentRoot)();
|
|
498
|
+
const postTurnObligations = (0, obligations_1.readPendingObligations)(agentRoot);
|
|
499
|
+
emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postTurnObligations, pendingObligations);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// Episode emission is non-fatal
|
|
503
|
+
}
|
|
504
|
+
// Step 6: postTurn
|
|
505
|
+
const continuingState = {
|
|
506
|
+
...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
|
|
507
|
+
...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
|
|
508
|
+
};
|
|
509
|
+
const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
|
|
510
|
+
? (typeof lastFriendActivityAt === "string"
|
|
511
|
+
? { lastFriendActivityAt }
|
|
512
|
+
: undefined)
|
|
513
|
+
: (Object.keys(continuingState).length > 0 ? continuingState : undefined);
|
|
514
|
+
input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
|
|
515
|
+
// Step 7: Token accumulation
|
|
516
|
+
await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
|
|
517
|
+
(0, runtime_1.emitNervesEvent)({
|
|
518
|
+
component: "senses",
|
|
519
|
+
event: "senses.pipeline_end",
|
|
520
|
+
message: "inbound turn pipeline completed",
|
|
521
|
+
meta: {
|
|
522
|
+
channel: input.channel,
|
|
523
|
+
friendId: resolvedContext.friend.id,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
// DRY cross-session awareness: notify inner dialog that activity happened on another channel
|
|
527
|
+
// Inner dialog's next checkpoint will include this session's state
|
|
528
|
+
if (input.channel !== "inner") {
|
|
498
529
|
try {
|
|
499
|
-
|
|
500
|
-
const postTurnObligations = (0, obligations_1.readPendingObligations)(agentRoot);
|
|
501
|
-
emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postTurnObligations, pendingObligations);
|
|
502
|
-
}
|
|
503
|
-
catch {
|
|
504
|
-
// Episode emission is non-fatal
|
|
530
|
+
(0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)()).catch(/* v8 ignore next */ () => { });
|
|
505
531
|
}
|
|
506
|
-
|
|
507
|
-
const continuingState = {
|
|
508
|
-
...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
|
|
509
|
-
...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
|
|
510
|
-
};
|
|
511
|
-
const nextState = result.outcome === "settled" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "observed"
|
|
512
|
-
? (typeof lastFriendActivityAt === "string"
|
|
513
|
-
? { lastFriendActivityAt }
|
|
514
|
-
: undefined)
|
|
515
|
-
: (Object.keys(continuingState).length > 0 ? continuingState : undefined);
|
|
516
|
-
input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
|
|
517
|
-
// Step 7: Token accumulation
|
|
518
|
-
await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
|
|
519
|
-
(0, runtime_1.emitNervesEvent)({
|
|
520
|
-
component: "senses",
|
|
521
|
-
event: "senses.pipeline_end",
|
|
522
|
-
message: "inbound turn pipeline completed",
|
|
523
|
-
meta: {
|
|
524
|
-
channel: input.channel,
|
|
525
|
-
friendId: resolvedContext.friend.id,
|
|
526
|
-
},
|
|
527
|
-
});
|
|
528
|
-
// DRY cross-session awareness: notify inner dialog that activity happened on another channel
|
|
529
|
-
// Inner dialog's next checkpoint will include this session's state
|
|
530
|
-
if (input.channel !== "inner") {
|
|
531
|
-
try {
|
|
532
|
-
(0, socket_client_1.requestInnerWake)((0, identity_1.getAgentName)()).catch(/* v8 ignore next */ () => { });
|
|
533
|
-
}
|
|
534
|
-
catch { /* getAgentName may fail in test environments */ }
|
|
535
|
-
}
|
|
536
|
-
return {
|
|
537
|
-
resolvedContext,
|
|
538
|
-
gateResult,
|
|
539
|
-
usage: result.usage,
|
|
540
|
-
turnOutcome: result.outcome,
|
|
541
|
-
completion: result.completion,
|
|
542
|
-
sessionPath: session.sessionPath,
|
|
543
|
-
messages: sessionMessages,
|
|
544
|
-
drainedPending: pending,
|
|
545
|
-
...(input.switchedProvider ? { switchedProvider: input.switchedProvider } : {}),
|
|
546
|
-
};
|
|
532
|
+
catch { /* getAgentName may fail in test environments */ }
|
|
547
533
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
534
|
+
return {
|
|
535
|
+
resolvedContext,
|
|
536
|
+
gateResult,
|
|
537
|
+
usage: result.usage,
|
|
538
|
+
turnOutcome: result.outcome,
|
|
539
|
+
completion: result.completion,
|
|
540
|
+
sessionPath: session.sessionPath,
|
|
541
|
+
messages: sessionMessages,
|
|
542
|
+
drainedPending: pending,
|
|
543
|
+
...(input.switchedProvider ? { switchedProvider: input.switchedProvider } : {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
// Step 6b: Post-turn sync push (opt-in, git-status-based discovery).
|
|
548
|
+
if (syncConfig.enabled) {
|
|
549
|
+
(0, sync_1.postTurnPush)((0, identity_1.getAgentRoot)(), syncConfig);
|
|
557
550
|
}
|
|
558
|
-
}
|
|
551
|
+
}
|
|
559
552
|
}
|