@ouro.bot/cli 0.1.0-alpha.48 → 0.1.0-alpha.49

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 CHANGED
@@ -1,6 +1,14 @@
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.49",
6
+ "changes": [
7
+ "Inner dialog no longer drops self-directed wake signals that arrive mid-turn; overlapping inward requests now guarantee a truthful follow-up inner pass instead of mailbox-like limbo.",
8
+ "The harness now has a shared core bridge layer with lifecycle/runtime state, persistence, task linkage, and one `bridge_manage` path so already-live sessions can coordinate as one coherent piece of work.",
9
+ "`query_session` and bridge attachment now share one session-recall helper, reducing duplicate cross-session summarization logic and setting up thinner outer surfaces around a stronger inner/core."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.48",
6
14
  "changes": [
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatBridgeStatus = formatBridgeStatus;
4
+ exports.formatBridgeContext = formatBridgeContext;
5
+ exports.createBridgeManager = createBridgeManager;
6
+ const runtime_1 = require("../../nerves/runtime");
7
+ const state_machine_1 = require("./state-machine");
8
+ const store_1 = require("./store");
9
+ const turn_coordinator_1 = require("../turn-coordinator");
10
+ const tasks_1 = require("../../repertoire/tasks");
11
+ function defaultIdFactory() {
12
+ return `bridge-${Date.now().toString(36)}`;
13
+ }
14
+ function sessionIdentityKey(session) {
15
+ return `${session.friendId}/${session.channel}/${session.key}`;
16
+ }
17
+ function assertBridgeMutable(bridge, action) {
18
+ if (bridge.lifecycle === "completed" || bridge.lifecycle === "cancelled") {
19
+ throw new Error(`cannot ${action} a terminal bridge`);
20
+ }
21
+ }
22
+ function defaultTaskBody(bridge) {
23
+ const lines = [
24
+ "## scope",
25
+ bridge.objective,
26
+ "",
27
+ "## bridge",
28
+ `id: ${bridge.id}`,
29
+ ];
30
+ if (bridge.attachedSessions.length > 0) {
31
+ lines.push("sessions:");
32
+ for (const session of bridge.attachedSessions) {
33
+ lines.push(`- ${sessionIdentityKey(session)}`);
34
+ }
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ function formatBridgeStatus(bridge) {
39
+ const summary = typeof bridge.summary === "string" ? bridge.summary.trim() : "";
40
+ const lines = [
41
+ `bridge: ${bridge.id}`,
42
+ `objective: ${bridge.objective}`,
43
+ `state: ${(0, state_machine_1.bridgeStateLabel)(bridge)}`,
44
+ `sessions: ${bridge.attachedSessions.length}`,
45
+ `task: ${bridge.task?.taskName ?? "none"}`,
46
+ ];
47
+ if (summary) {
48
+ lines.push(`summary: ${summary}`);
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ function formatBridgeContext(bridges) {
53
+ if (bridges.length === 0)
54
+ return "";
55
+ const lines = ["## active bridge work"];
56
+ for (const bridge of bridges) {
57
+ const task = bridge.task?.taskName ? ` (task: ${bridge.task.taskName})` : "";
58
+ const label = typeof bridge.summary === "string" && bridge.summary.trim().length > 0 ? bridge.summary.trim() : bridge.objective;
59
+ lines.push(`- ${bridge.id}: ${label} [${(0, state_machine_1.bridgeStateLabel)(bridge)}]${task}`);
60
+ }
61
+ return lines.join("\n");
62
+ }
63
+ function ensureRunnable(bridge, now, store) {
64
+ if (bridge.lifecycle === "forming" || bridge.lifecycle === "suspended") {
65
+ const activated = {
66
+ ...bridge,
67
+ ...(0, state_machine_1.activateBridge)(bridge),
68
+ updatedAt: now(),
69
+ };
70
+ return store.save(activated);
71
+ }
72
+ if (bridge.lifecycle === "completed" || bridge.lifecycle === "cancelled") {
73
+ throw new Error(`bridge is terminal: ${bridge.id}`);
74
+ }
75
+ return bridge;
76
+ }
77
+ function createBridgeManager(options = {}) {
78
+ const store = options.store ?? (0, store_1.createBridgeStore)();
79
+ const now = options.now ?? (() => new Date().toISOString());
80
+ const idFactory = options.idFactory ?? defaultIdFactory;
81
+ function requireBridge(bridgeId) {
82
+ const bridge = store.get(bridgeId);
83
+ if (!bridge) {
84
+ throw new Error(`bridge not found: ${bridgeId}`);
85
+ }
86
+ return bridge;
87
+ }
88
+ function save(bridge) {
89
+ return store.save(bridge);
90
+ }
91
+ return {
92
+ beginBridge(input) {
93
+ const timestamp = now();
94
+ const state = (0, state_machine_1.activateBridge)((0, state_machine_1.createBridgeState)());
95
+ const bridge = {
96
+ id: idFactory(),
97
+ objective: input.objective,
98
+ summary: input.summary,
99
+ lifecycle: state.lifecycle,
100
+ runtime: state.runtime,
101
+ createdAt: timestamp,
102
+ updatedAt: timestamp,
103
+ attachedSessions: [input.session],
104
+ task: null,
105
+ };
106
+ (0, runtime_1.emitNervesEvent)({
107
+ component: "engine",
108
+ event: "engine.bridge_begin",
109
+ message: "created bridge",
110
+ meta: {
111
+ bridgeId: bridge.id,
112
+ session: sessionIdentityKey(input.session),
113
+ },
114
+ });
115
+ return save(bridge);
116
+ },
117
+ attachSession(bridgeId, session) {
118
+ const bridge = requireBridge(bridgeId);
119
+ assertBridgeMutable(bridge, "attach session to");
120
+ const existing = bridge.attachedSessions.some((candidate) => sessionIdentityKey(candidate) === sessionIdentityKey(session));
121
+ if (existing)
122
+ return bridge;
123
+ const updated = {
124
+ ...bridge,
125
+ attachedSessions: [...bridge.attachedSessions, session],
126
+ updatedAt: now(),
127
+ };
128
+ (0, runtime_1.emitNervesEvent)({
129
+ component: "engine",
130
+ event: "engine.bridge_attach_session",
131
+ message: "attached canonical session to bridge",
132
+ meta: {
133
+ bridgeId,
134
+ session: sessionIdentityKey(session),
135
+ },
136
+ });
137
+ return save(updated);
138
+ },
139
+ detachSession(bridgeId, session) {
140
+ const bridge = requireBridge(bridgeId);
141
+ assertBridgeMutable(bridge, "detach session from");
142
+ const updated = {
143
+ ...bridge,
144
+ attachedSessions: bridge.attachedSessions.filter((candidate) => sessionIdentityKey(candidate) !== sessionIdentityKey(session)),
145
+ updatedAt: now(),
146
+ };
147
+ (0, runtime_1.emitNervesEvent)({
148
+ component: "engine",
149
+ event: "engine.bridge_detach_session",
150
+ message: "detached canonical session from bridge",
151
+ meta: {
152
+ bridgeId,
153
+ session: sessionIdentityKey(session),
154
+ },
155
+ });
156
+ return save(updated);
157
+ },
158
+ getBridge(bridgeId) {
159
+ return store.get(bridgeId);
160
+ },
161
+ listBridges() {
162
+ return store.list();
163
+ },
164
+ findBridgesForSession(session) {
165
+ return store.findBySession(session)
166
+ .filter((bridge) => bridge.lifecycle !== "completed" && bridge.lifecycle !== "cancelled");
167
+ },
168
+ promoteBridgeToTask(bridgeId, input = {}) {
169
+ const bridge = requireBridge(bridgeId);
170
+ assertBridgeMutable(bridge, "promote");
171
+ if (bridge.task)
172
+ return bridge;
173
+ const taskPath = (0, tasks_1.getTaskModule)().createTask({
174
+ title: input.title?.trim() || bridge.objective,
175
+ type: "ongoing",
176
+ category: input.category?.trim() || "coordination",
177
+ status: "processing",
178
+ body: input.body?.trim() || defaultTaskBody(bridge),
179
+ activeBridge: bridge.id,
180
+ bridgeSessions: bridge.attachedSessions.map((session) => sessionIdentityKey(session)),
181
+ });
182
+ const taskName = taskPath.replace(/^.*\//, "").replace(/\.md$/, "");
183
+ const updated = save({
184
+ ...bridge,
185
+ task: {
186
+ taskName,
187
+ path: taskPath,
188
+ mode: "promoted",
189
+ boundAt: now(),
190
+ },
191
+ updatedAt: now(),
192
+ });
193
+ (0, runtime_1.emitNervesEvent)({
194
+ component: "engine",
195
+ event: "engine.bridge_promote_task",
196
+ message: "promoted bridge to task-backed work",
197
+ meta: {
198
+ bridgeId,
199
+ taskName,
200
+ },
201
+ });
202
+ return updated;
203
+ },
204
+ completeBridge(bridgeId) {
205
+ const bridge = requireBridge(bridgeId);
206
+ const updated = save({
207
+ ...bridge,
208
+ ...(0, state_machine_1.completeBridge)(bridge),
209
+ updatedAt: now(),
210
+ });
211
+ (0, runtime_1.emitNervesEvent)({
212
+ component: "engine",
213
+ event: "engine.bridge_complete",
214
+ message: "completed bridge",
215
+ meta: {
216
+ bridgeId,
217
+ },
218
+ });
219
+ return updated;
220
+ },
221
+ cancelBridge(bridgeId) {
222
+ const bridge = requireBridge(bridgeId);
223
+ const updated = save({
224
+ ...bridge,
225
+ ...(0, state_machine_1.cancelBridge)(bridge),
226
+ updatedAt: now(),
227
+ });
228
+ (0, runtime_1.emitNervesEvent)({
229
+ component: "engine",
230
+ event: "engine.bridge_cancel",
231
+ message: "cancelled bridge",
232
+ meta: {
233
+ bridgeId,
234
+ },
235
+ });
236
+ return updated;
237
+ },
238
+ async runBridgeTurn(bridgeId, fn) {
239
+ if (!(0, turn_coordinator_1.tryBeginSharedTurn)("bridge", bridgeId)) {
240
+ const bridge = requireBridge(bridgeId);
241
+ const queued = bridge.runtime === "awaiting-follow-up"
242
+ ? bridge
243
+ : save({
244
+ ...bridge,
245
+ ...(0, state_machine_1.queueBridgeFollowUp)(bridge),
246
+ updatedAt: now(),
247
+ });
248
+ (0, turn_coordinator_1.enqueueSharedFollowUp)("bridge", bridgeId, {
249
+ conversationId: bridgeId,
250
+ text: "bridge follow-up",
251
+ receivedAt: Date.now(),
252
+ effect: "none",
253
+ });
254
+ (0, runtime_1.emitNervesEvent)({
255
+ component: "engine",
256
+ event: "engine.bridge_turn_queued",
257
+ message: "queued follow-up bridge turn",
258
+ meta: {
259
+ bridgeId,
260
+ },
261
+ });
262
+ return {
263
+ queued: true,
264
+ bridge: queued,
265
+ };
266
+ }
267
+ try {
268
+ let current = ensureRunnable(requireBridge(bridgeId), now, store);
269
+ current = save({
270
+ ...current,
271
+ ...(0, state_machine_1.beginBridgeProcessing)(current),
272
+ updatedAt: now(),
273
+ });
274
+ while (true) {
275
+ (0, runtime_1.emitNervesEvent)({
276
+ component: "engine",
277
+ event: "engine.bridge_turn_start",
278
+ message: "running bridge turn",
279
+ meta: {
280
+ bridgeId,
281
+ },
282
+ });
283
+ await fn();
284
+ let next = requireBridge(bridgeId);
285
+ const bufferedFollowUps = (0, turn_coordinator_1.drainSharedFollowUps)("bridge", bridgeId);
286
+ if (bufferedFollowUps.length > 0 && next.runtime !== "awaiting-follow-up") {
287
+ next = save({
288
+ ...next,
289
+ ...(0, state_machine_1.queueBridgeFollowUp)(next),
290
+ updatedAt: now(),
291
+ });
292
+ }
293
+ const advanced = save({
294
+ ...next,
295
+ ...(0, state_machine_1.advanceBridgeAfterTurn)(next),
296
+ updatedAt: now(),
297
+ });
298
+ if (advanced.runtime === "processing") {
299
+ current = advanced;
300
+ continue;
301
+ }
302
+ (0, runtime_1.emitNervesEvent)({
303
+ component: "engine",
304
+ event: "engine.bridge_turn_end",
305
+ message: "bridge turn finished",
306
+ meta: {
307
+ bridgeId,
308
+ },
309
+ });
310
+ return {
311
+ queued: false,
312
+ bridge: current = advanced,
313
+ };
314
+ }
315
+ }
316
+ finally {
317
+ (0, turn_coordinator_1.endSharedTurn)("bridge", bridgeId);
318
+ }
319
+ },
320
+ };
321
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createBridgeState = createBridgeState;
4
+ exports.bridgeStateLabel = bridgeStateLabel;
5
+ exports.activateBridge = activateBridge;
6
+ exports.beginBridgeProcessing = beginBridgeProcessing;
7
+ exports.queueBridgeFollowUp = queueBridgeFollowUp;
8
+ exports.advanceBridgeAfterTurn = advanceBridgeAfterTurn;
9
+ exports.suspendBridge = suspendBridge;
10
+ exports.completeBridge = completeBridge;
11
+ exports.cancelBridge = cancelBridge;
12
+ const runtime_1 = require("../../nerves/runtime");
13
+ function transition(state, next, action) {
14
+ (0, runtime_1.emitNervesEvent)({
15
+ component: "engine",
16
+ event: "engine.bridge_state_transition",
17
+ message: "bridge state transitioned",
18
+ meta: {
19
+ action,
20
+ from: bridgeStateLabel(state),
21
+ to: bridgeStateLabel(next),
22
+ },
23
+ });
24
+ return next;
25
+ }
26
+ function assertNonTerminal(state, action) {
27
+ if (state.lifecycle === "completed" || state.lifecycle === "cancelled") {
28
+ throw new Error(`cannot ${action} a terminal bridge`);
29
+ }
30
+ }
31
+ function createBridgeState() {
32
+ return {
33
+ lifecycle: "forming",
34
+ runtime: "idle",
35
+ };
36
+ }
37
+ function bridgeStateLabel(state) {
38
+ switch (state.lifecycle) {
39
+ case "forming":
40
+ return "forming";
41
+ case "suspended":
42
+ return "suspended";
43
+ case "completed":
44
+ return "completed";
45
+ case "cancelled":
46
+ return "cancelled";
47
+ case "active":
48
+ if (state.runtime === "processing")
49
+ return "active-processing";
50
+ if (state.runtime === "awaiting-follow-up")
51
+ return "awaiting-follow-up";
52
+ return "active-idle";
53
+ }
54
+ }
55
+ function activateBridge(state) {
56
+ assertNonTerminal(state, "activate");
57
+ if (state.lifecycle !== "forming" && state.lifecycle !== "suspended") {
58
+ throw new Error("cannot activate bridge from current state");
59
+ }
60
+ return transition(state, { lifecycle: "active", runtime: "idle" }, "activate");
61
+ }
62
+ function beginBridgeProcessing(state) {
63
+ assertNonTerminal(state, "process");
64
+ if (state.lifecycle !== "active" || state.runtime !== "idle") {
65
+ throw new Error("cannot process bridge from current state");
66
+ }
67
+ return transition(state, { lifecycle: "active", runtime: "processing" }, "begin-processing");
68
+ }
69
+ function queueBridgeFollowUp(state) {
70
+ assertNonTerminal(state, "queue");
71
+ if (state.lifecycle !== "active") {
72
+ throw new Error("cannot queue follow-up for non-active bridge");
73
+ }
74
+ if (state.runtime === "processing") {
75
+ return transition(state, { lifecycle: "active", runtime: "awaiting-follow-up" }, "queue-follow-up");
76
+ }
77
+ if (state.runtime === "awaiting-follow-up") {
78
+ return state;
79
+ }
80
+ throw new Error("cannot queue follow-up when bridge is not processing");
81
+ }
82
+ function advanceBridgeAfterTurn(state) {
83
+ assertNonTerminal(state, "advance");
84
+ if (state.lifecycle !== "active") {
85
+ throw new Error("cannot advance non-active bridge");
86
+ }
87
+ if (state.runtime === "processing") {
88
+ return transition(state, { lifecycle: "active", runtime: "idle" }, "finish-processing");
89
+ }
90
+ if (state.runtime === "awaiting-follow-up") {
91
+ return transition(state, { lifecycle: "active", runtime: "processing" }, "resume-follow-up");
92
+ }
93
+ throw new Error("cannot advance an idle bridge");
94
+ }
95
+ function suspendBridge(state) {
96
+ assertNonTerminal(state, "suspend");
97
+ if ((state.lifecycle !== "forming" && state.lifecycle !== "active") || state.runtime !== "idle") {
98
+ throw new Error("cannot suspend bridge from current state");
99
+ }
100
+ return transition(state, { lifecycle: "suspended", runtime: "idle" }, "suspend");
101
+ }
102
+ function completeBridge(state) {
103
+ assertNonTerminal(state, "complete");
104
+ if (state.runtime !== "idle") {
105
+ throw new Error("cannot complete a bridge mid-turn");
106
+ }
107
+ return transition(state, { lifecycle: "completed", runtime: "idle" }, "complete");
108
+ }
109
+ function cancelBridge(state) {
110
+ assertNonTerminal(state, "cancel");
111
+ if (state.runtime !== "idle") {
112
+ throw new Error("cannot cancel a bridge mid-turn");
113
+ }
114
+ return transition(state, { lifecycle: "cancelled", runtime: "idle" }, "cancel");
115
+ }
@@ -0,0 +1,123 @@
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.getBridgeStateRoot = getBridgeStateRoot;
37
+ exports.createBridgeStore = createBridgeStore;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const identity_1 = require("../identity");
41
+ const runtime_1 = require("../../nerves/runtime");
42
+ function sessionIdentityMatches(session, candidate) {
43
+ return (session.friendId === candidate.friendId
44
+ && session.channel === candidate.channel
45
+ && session.key === candidate.key);
46
+ }
47
+ function bridgeFilePath(rootDir, id) {
48
+ return path.join(rootDir, `${id}.json`);
49
+ }
50
+ function getBridgeStateRoot() {
51
+ return path.join((0, identity_1.getAgentStateRoot)(), "bridges");
52
+ }
53
+ function createBridgeStore(options = {}) {
54
+ const rootDir = options.rootDir ?? getBridgeStateRoot();
55
+ function ensureRoot() {
56
+ fs.mkdirSync(rootDir, { recursive: true });
57
+ }
58
+ return {
59
+ save(bridge) {
60
+ ensureRoot();
61
+ fs.writeFileSync(bridgeFilePath(rootDir, bridge.id), JSON.stringify(bridge, null, 2), "utf-8");
62
+ (0, runtime_1.emitNervesEvent)({
63
+ component: "engine",
64
+ event: "engine.bridge_store_save",
65
+ message: "saved bridge record",
66
+ meta: {
67
+ bridgeId: bridge.id,
68
+ rootDir,
69
+ },
70
+ });
71
+ return bridge;
72
+ },
73
+ get(id) {
74
+ const filePath = bridgeFilePath(rootDir, id);
75
+ try {
76
+ const raw = fs.readFileSync(filePath, "utf-8");
77
+ return JSON.parse(raw);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ },
83
+ list() {
84
+ ensureRoot();
85
+ const files = fs.readdirSync(rootDir).filter((entry) => entry.endsWith(".json")).sort();
86
+ const bridges = files
87
+ .map((fileName) => {
88
+ try {
89
+ return JSON.parse(fs.readFileSync(path.join(rootDir, fileName), "utf-8"));
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ })
95
+ .filter((bridge) => bridge !== null);
96
+ (0, runtime_1.emitNervesEvent)({
97
+ component: "engine",
98
+ event: "engine.bridge_store_list",
99
+ message: "listed bridge records",
100
+ meta: {
101
+ rootDir,
102
+ count: bridges.length,
103
+ },
104
+ });
105
+ return bridges;
106
+ },
107
+ findBySession(session) {
108
+ const matches = this.list().filter((bridge) => bridge.attachedSessions.some((candidate) => sessionIdentityMatches(session, candidate)));
109
+ (0, runtime_1.emitNervesEvent)({
110
+ component: "engine",
111
+ event: "engine.bridge_store_find_by_session",
112
+ message: "located bridges for canonical session",
113
+ meta: {
114
+ friendId: session.friendId,
115
+ channel: session.channel,
116
+ key: session.key,
117
+ count: matches.length,
118
+ },
119
+ });
120
+ return matches;
121
+ },
122
+ };
123
+ }
@@ -151,8 +151,16 @@ function formatSurfacedValue(text, maxLength = 120) {
151
151
  }
152
152
  function deriveInnerDialogStatus(pendingMessages, turns, runtimeState) {
153
153
  if (runtimeState?.status === "running") {
154
+ if (pendingMessages.length > 0) {
155
+ return {
156
+ queue: "queued to inner/dialog",
157
+ wake: "queued behind active turn",
158
+ processing: "pending",
159
+ surfaced: "nothing yet",
160
+ };
161
+ }
154
162
  return {
155
- queue: pendingMessages.length > 0 ? "queued to inner/dialog" : "clear",
163
+ queue: "clear",
156
164
  wake: "in progress",
157
165
  processing: "started",
158
166
  surfaced: "nothing yet",
@@ -0,0 +1,116 @@
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.recallSession = recallSession;
37
+ const fs = __importStar(require("fs"));
38
+ const runtime_1 = require("../nerves/runtime");
39
+ function normalizeContent(content) {
40
+ if (typeof content === "string")
41
+ return content;
42
+ if (!Array.isArray(content))
43
+ return "";
44
+ return content
45
+ .map((part) => (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part
46
+ ? String(part.text ?? "")
47
+ : ""))
48
+ .filter((text) => text.length > 0)
49
+ .join("");
50
+ }
51
+ function buildSummaryInstruction(friendId, channel, trustLevel) {
52
+ if (friendId === "self" && channel === "inner") {
53
+ return "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning.";
54
+ }
55
+ return `summarize this session transcript. the person asking has trust level: ${trustLevel}. family=full transparency, friend=share work and general topics but protect other people's identities, acquaintance=very guarded minimal disclosure.`;
56
+ }
57
+ function clip(text, limit = 160) {
58
+ const compact = text.replace(/\s+/g, " ").trim();
59
+ return compact.length > limit ? compact.slice(0, limit - 1) + "…" : compact;
60
+ }
61
+ function buildSnapshot(summary, tailMessages) {
62
+ const lines = [`recent focus: ${clip(summary, 240)}`];
63
+ const latestUser = [...tailMessages].reverse().find((message) => message.role === "user")?.content;
64
+ const latestAssistant = [...tailMessages].reverse().find((message) => message.role === "assistant")?.content;
65
+ if (latestUser) {
66
+ lines.push(`latest user: ${clip(latestUser)}`);
67
+ }
68
+ if (latestAssistant) {
69
+ lines.push(`latest assistant: ${clip(latestAssistant)}`);
70
+ }
71
+ return lines.join("\n");
72
+ }
73
+ async function recallSession(options) {
74
+ (0, runtime_1.emitNervesEvent)({
75
+ component: "daemon",
76
+ event: "daemon.session_recall",
77
+ message: "recalling session transcript tail",
78
+ meta: {
79
+ friendId: options.friendId,
80
+ channel: options.channel,
81
+ key: options.key,
82
+ messageCount: options.messageCount,
83
+ },
84
+ });
85
+ let raw;
86
+ try {
87
+ raw = fs.readFileSync(options.sessionPath, "utf-8");
88
+ }
89
+ catch {
90
+ return { kind: "missing" };
91
+ }
92
+ const parsed = JSON.parse(raw);
93
+ const tailMessages = (parsed.messages ?? [])
94
+ .map((message) => ({
95
+ role: typeof message.role === "string" ? message.role : "",
96
+ content: normalizeContent(message.content),
97
+ }))
98
+ .filter((message) => message.role !== "system" && message.content.length > 0)
99
+ .slice(-options.messageCount);
100
+ if (tailMessages.length === 0) {
101
+ return { kind: "empty" };
102
+ }
103
+ const transcript = tailMessages
104
+ .map((message) => `[${message.role}] ${message.content}`)
105
+ .join("\n");
106
+ const summary = options.summarize
107
+ ? await options.summarize(transcript, buildSummaryInstruction(options.friendId, options.channel, options.trustLevel ?? "family"))
108
+ : transcript;
109
+ return {
110
+ kind: "ok",
111
+ transcript,
112
+ summary,
113
+ snapshot: buildSnapshot(summary, tailMessages),
114
+ tailMessages,
115
+ };
116
+ }
@@ -2,7 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createTurnCoordinator = createTurnCoordinator;
4
4
  exports.withSharedTurnLock = withSharedTurnLock;
5
+ exports.tryBeginSharedTurn = tryBeginSharedTurn;
6
+ exports.endSharedTurn = endSharedTurn;
7
+ exports.isSharedTurnActive = isSharedTurnActive;
8
+ exports.enqueueSharedFollowUp = enqueueSharedFollowUp;
9
+ exports.drainSharedFollowUps = drainSharedFollowUps;
5
10
  const runtime_1 = require("../nerves/runtime");
11
+ function scopedKey(scope, key) {
12
+ return `${scope}:${key}`;
13
+ }
6
14
  function createTurnCoordinator() {
7
15
  const turnLocks = new Map();
8
16
  const activeTurns = new Set();
@@ -63,5 +71,20 @@ function createTurnCoordinator() {
63
71
  }
64
72
  const _sharedTurnCoordinator = createTurnCoordinator();
65
73
  function withSharedTurnLock(scope, key, fn) {
66
- return _sharedTurnCoordinator.withTurnLock(`${scope}:${key}`, fn);
74
+ return _sharedTurnCoordinator.withTurnLock(scopedKey(scope, key), fn);
75
+ }
76
+ function tryBeginSharedTurn(scope, key) {
77
+ return _sharedTurnCoordinator.tryBeginTurn(scopedKey(scope, key));
78
+ }
79
+ function endSharedTurn(scope, key) {
80
+ _sharedTurnCoordinator.endTurn(scopedKey(scope, key));
81
+ }
82
+ function isSharedTurnActive(scope, key) {
83
+ return _sharedTurnCoordinator.isTurnActive(scopedKey(scope, key));
84
+ }
85
+ function enqueueSharedFollowUp(scope, key, followUp) {
86
+ _sharedTurnCoordinator.enqueueFollowUp(scopedKey(scope, key), followUp);
87
+ }
88
+ function drainSharedFollowUps(scope, key) {
89
+ return _sharedTurnCoordinator.drainFollowUps(scopedKey(scope, key));
67
90
  }
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.INNER_DIALOG_PENDING = void 0;
37
37
  exports.getPendingDir = getPendingDir;
38
38
  exports.getInnerDialogPendingDir = getInnerDialogPendingDir;
39
+ exports.hasPendingMessages = hasPendingMessages;
39
40
  exports.drainPending = drainPending;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
@@ -50,6 +51,16 @@ exports.INNER_DIALOG_PENDING = { friendId: "self", channel: "inner", key: "dialo
50
51
  function getInnerDialogPendingDir(agentName) {
51
52
  return getPendingDir(agentName, exports.INNER_DIALOG_PENDING.friendId, exports.INNER_DIALOG_PENDING.channel, exports.INNER_DIALOG_PENDING.key);
52
53
  }
54
+ function hasPendingMessages(pendingDir) {
55
+ if (!fs.existsSync(pendingDir))
56
+ return false;
57
+ try {
58
+ return fs.readdirSync(pendingDir).some((entry) => entry.endsWith(".json") || entry.endsWith(".json.processing"));
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
53
64
  function drainPending(pendingDir) {
54
65
  if (!fs.existsSync(pendingDir))
55
66
  return [];
@@ -412,6 +412,12 @@ function memoryFriendToolContractSection() {
412
412
  - My psyche files (SOUL, IDENTITY, TACIT, LORE, ASPIRATIONS) are always loaded - I already know who I am.
413
413
  - My task board is always loaded - I already know my work.`;
414
414
  }
415
+ function bridgeContextSection(options) {
416
+ const bridgeContext = options?.bridgeContext?.trim() ?? "";
417
+ if (!bridgeContext)
418
+ return "";
419
+ return bridgeContext.startsWith("## ") ? bridgeContext : `## active bridge work\n${bridgeContext}`;
420
+ }
415
421
  function toolBehaviorSection(options) {
416
422
  if (!(options?.toolChoiceRequired ?? true))
417
423
  return "";
@@ -543,13 +549,14 @@ async function buildSystem(channel = "cli", options, context) {
543
549
  mixedTrustGroupSection(context),
544
550
  skillsSection(),
545
551
  taskBoardSection(),
552
+ bridgeContextSection(options),
546
553
  buildSessionSummary({
547
554
  sessionsDir: path.join((0, identity_1.getAgentRoot)(), "state", "sessions"),
548
555
  friendsDir: path.join((0, identity_1.getAgentRoot)(), "friends"),
549
556
  agentName: (0, identity_1.getAgentName)(),
550
557
  currentFriendId: context?.friend?.id,
551
558
  currentChannel: channel,
552
- currentKey: "session",
559
+ currentKey: options?.currentSessionKey ?? "session",
553
560
  }),
554
561
  memoryFriendToolContractSection(),
555
562
  toolBehaviorSection(options),
@@ -59,6 +59,12 @@ function activeSessionLines(tasks) {
59
59
  });
60
60
  return active.map((task) => task.stem).sort();
61
61
  }
62
+ function activeBridgeLines(tasks) {
63
+ return tasks
64
+ .filter((task) => typeof task.frontmatter.active_bridge === "string" && String(task.frontmatter.active_bridge).trim())
65
+ .map((task) => `${task.stem} -> ${String(task.frontmatter.active_bridge).trim()}`)
66
+ .sort();
67
+ }
62
68
  function actionRequired(index, byStatus) {
63
69
  const actions = [...index.parseErrors, ...index.invalidFilenames.map((filePath) => `bad filename: ${filePath}`)];
64
70
  if (byStatus.blocked.length > 0) {
@@ -99,6 +105,11 @@ function buildTaskBoard(index) {
99
105
  fullLines.push("## active sessions");
100
106
  fullLines.push(active.map((line) => `- ${line}`).join("\n"));
101
107
  }
108
+ const activeBridges = activeBridgeLines(index.tasks);
109
+ if (activeBridges.length > 0) {
110
+ fullLines.push("## active bridges");
111
+ fullLines.push(activeBridges.map((line) => `- ${line}`).join("\n"));
112
+ }
102
113
  return {
103
114
  compact,
104
115
  full: fullLines.join("\n\n"),
@@ -106,6 +117,7 @@ function buildTaskBoard(index) {
106
117
  actionRequired: actionRequired(index, byStatus),
107
118
  unresolvedDependencies: unresolved,
108
119
  activeSessions: active,
120
+ activeBridges,
109
121
  };
110
122
  }
111
123
  function boardStatus(board, status) {
@@ -115,6 +115,12 @@ class FileTaskModule {
115
115
  frontmatter.parent_task = null;
116
116
  frontmatter.depends_on = [];
117
117
  }
118
+ if (input.activeBridge && input.activeBridge.trim()) {
119
+ frontmatter.active_bridge = input.activeBridge.trim();
120
+ }
121
+ if (Array.isArray(input.bridgeSessions) && input.bridgeSessions.length > 0) {
122
+ frontmatter.bridge_sessions = input.bridgeSessions.filter((value) => typeof value === "string" && value.trim());
123
+ }
118
124
  const content = (0, parser_1.renderTaskFile)(frontmatter, input.body);
119
125
  const validation = (0, middleware_1.validateWrite)(filePath, content);
120
126
  if (!validation.ok) {
@@ -125,6 +131,21 @@ class FileTaskModule {
125
131
  (0, scanner_1.clearTaskScanCache)();
126
132
  return filePath;
127
133
  }
134
+ bindBridge(name, input) {
135
+ const task = this.getTask(name);
136
+ if (!task) {
137
+ return { ok: false, reason: `task not found: ${name}` };
138
+ }
139
+ const content = fs.readFileSync(task.path, "utf-8");
140
+ const parsed = (0, parser_1.parseTaskFile)(content, task.path);
141
+ const frontmatter = removeRuntimeFrontmatter(parsed.frontmatter);
142
+ frontmatter.active_bridge = input.bridgeId.trim();
143
+ frontmatter.bridge_sessions = input.sessionRefs.filter((value) => value.trim().length > 0);
144
+ frontmatter.updated = formatDate();
145
+ fs.writeFileSync(task.path, (0, parser_1.renderTaskFile)(frontmatter, parsed.body), "utf-8");
146
+ (0, scanner_1.clearTaskScanCache)();
147
+ return { ok: true, path: task.path };
148
+ }
128
149
  updateStatus(name, toStatus) {
129
150
  const normalized = (0, transitions_1.normalizeTaskStatus)(toStatus);
130
151
  if (!normalized) {
@@ -44,6 +44,8 @@ const runtime_1 = require("../nerves/runtime");
44
44
  const identity_1 = require("../heart/identity");
45
45
  const socket_client_1 = require("../heart/daemon/socket-client");
46
46
  const thoughts_1 = require("../heart/daemon/thoughts");
47
+ const manager_1 = require("../heart/bridges/manager");
48
+ const session_recall_1 = require("../heart/session-recall");
47
49
  const tools_1 = require("./coding/tools");
48
50
  const memory_1 = require("../mind/memory");
49
51
  const pending_1 = require("../mind/pending");
@@ -61,6 +63,16 @@ function buildContextDiff(lines, changeStart, changeEnd, contextSize = 3) {
61
63
  }
62
64
  return result.join("\n");
63
65
  }
66
+ const NO_SESSION_FOUND_MESSAGE = "no session found for that friend/channel/key combination.";
67
+ const EMPTY_SESSION_MESSAGE = "session exists but has no non-system messages.";
68
+ async function recallSessionSafely(options) {
69
+ try {
70
+ return await (0, session_recall_1.recallSession)(options);
71
+ }
72
+ catch {
73
+ return { kind: "missing" };
74
+ }
75
+ }
64
76
  exports.baseToolDefinitions = [
65
77
  {
66
78
  tool: {
@@ -585,6 +597,103 @@ exports.baseToolDefinitions = [
585
597
  },
586
598
  },
587
599
  // -- cross-session awareness --
600
+ {
601
+ tool: {
602
+ type: "function",
603
+ function: {
604
+ name: "bridge_manage",
605
+ description: "create and manage shared live-work bridges across already-active sessions.",
606
+ parameters: {
607
+ type: "object",
608
+ properties: {
609
+ action: {
610
+ type: "string",
611
+ enum: ["begin", "attach", "status", "promote_task", "complete", "cancel"],
612
+ },
613
+ bridgeId: { type: "string", description: "bridge id for all actions except begin" },
614
+ objective: { type: "string", description: "objective for begin" },
615
+ summary: { type: "string", description: "optional concise shared-work summary" },
616
+ friendId: { type: "string", description: "target friend id for attach" },
617
+ channel: { type: "string", description: "target channel for attach" },
618
+ key: { type: "string", description: "target session key for attach (defaults to 'session')" },
619
+ title: { type: "string", description: "task title override for promote_task" },
620
+ category: { type: "string", description: "task category override for promote_task" },
621
+ body: { type: "string", description: "task body override for promote_task" },
622
+ },
623
+ required: ["action"],
624
+ },
625
+ },
626
+ },
627
+ handler: async (args, ctx) => {
628
+ const manager = (0, manager_1.createBridgeManager)();
629
+ const action = (args.action || "").trim();
630
+ if (action === "begin") {
631
+ if (!ctx?.currentSession) {
632
+ return "bridge_manage begin requires an active session context.";
633
+ }
634
+ const objective = (args.objective || "").trim();
635
+ if (!objective)
636
+ return "objective is required for bridge begin.";
637
+ return (0, manager_1.formatBridgeStatus)(manager.beginBridge({
638
+ objective,
639
+ summary: (args.summary || objective).trim(),
640
+ session: ctx.currentSession,
641
+ }));
642
+ }
643
+ const bridgeId = (args.bridgeId || "").trim();
644
+ if (!bridgeId) {
645
+ return "bridgeId is required for this bridge action.";
646
+ }
647
+ if (action === "attach") {
648
+ const friendId = (args.friendId || "").trim();
649
+ const channel = (args.channel || "").trim();
650
+ const key = (args.key || "session").trim();
651
+ if (!friendId || !channel) {
652
+ return "friendId and channel are required for bridge attach.";
653
+ }
654
+ const sessionPath = (0, config_1.resolveSessionPath)(friendId, channel, key);
655
+ const recall = await recallSessionSafely({
656
+ sessionPath,
657
+ friendId,
658
+ channel,
659
+ key,
660
+ messageCount: 20,
661
+ trustLevel: ctx?.context?.friend?.trustLevel,
662
+ summarize: ctx?.summarize,
663
+ });
664
+ if (recall.kind === "missing") {
665
+ return NO_SESSION_FOUND_MESSAGE;
666
+ }
667
+ return (0, manager_1.formatBridgeStatus)(manager.attachSession(bridgeId, {
668
+ friendId,
669
+ channel,
670
+ key,
671
+ sessionPath,
672
+ snapshot: recall.kind === "ok" ? recall.snapshot : EMPTY_SESSION_MESSAGE,
673
+ }));
674
+ }
675
+ if (action === "status") {
676
+ const bridge = manager.getBridge(bridgeId);
677
+ if (!bridge)
678
+ return `bridge not found: ${bridgeId}`;
679
+ return (0, manager_1.formatBridgeStatus)(bridge);
680
+ }
681
+ if (action === "promote_task") {
682
+ return (0, manager_1.formatBridgeStatus)(manager.promoteBridgeToTask(bridgeId, {
683
+ title: args.title,
684
+ category: args.category,
685
+ body: args.body,
686
+ }));
687
+ }
688
+ if (action === "complete") {
689
+ return (0, manager_1.formatBridgeStatus)(manager.completeBridge(bridgeId));
690
+ }
691
+ if (action === "cancel") {
692
+ return (0, manager_1.formatBridgeStatus)(manager.cancelBridge(bridgeId));
693
+ }
694
+ return `unknown bridge action: ${action}`;
695
+ },
696
+ },
588
697
  {
589
698
  tool: {
590
699
  type: "function",
@@ -605,43 +714,36 @@ exports.baseToolDefinitions = [
605
714
  },
606
715
  },
607
716
  handler: async (args, ctx) => {
608
- try {
609
- const friendId = args.friendId;
610
- const channel = args.channel;
611
- const key = args.key || "session";
612
- const count = parseInt(args.messageCount || "20", 10);
613
- const mode = args.mode || "transcript";
614
- if (mode === "status") {
615
- if (friendId !== "self" || channel !== "inner") {
616
- return "status mode is only available for self/inner dialog.";
617
- }
618
- const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
619
- const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
620
- return (0, thoughts_1.formatInnerDialogStatus)((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
621
- }
622
- const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
623
- const raw = fs.readFileSync(sessFile, "utf-8");
624
- const data = JSON.parse(raw);
625
- const messages = (data.messages || [])
626
- .filter((m) => m.role !== "system");
627
- const tail = messages.slice(-count);
628
- if (tail.length === 0)
629
- return "session exists but has no non-system messages.";
630
- const transcript = tail.map((m) => `[${m.role}] ${m.content}`).join("\n");
631
- // LLM summarization when summarize function is available
632
- if (ctx?.summarize) {
633
- const trustLevel = ctx.context?.friend?.trustLevel ?? "family";
634
- const isSelfQuery = friendId === "self";
635
- const instruction = isSelfQuery
636
- ? "summarize this session transcript fully and transparently. this is my own inner dialog — include all details, decisions, and reasoning."
637
- : `summarize this session transcript. the person asking has trust level: ${trustLevel}. family=full transparency, friend=share work and general topics but protect other people's identities, acquaintance=very guarded minimal disclosure.`;
638
- return await ctx.summarize(transcript, instruction);
717
+ const friendId = args.friendId;
718
+ const channel = args.channel;
719
+ const key = args.key || "session";
720
+ const count = parseInt(args.messageCount || "20", 10);
721
+ const mode = args.mode || "transcript";
722
+ if (mode === "status") {
723
+ if (friendId !== "self" || channel !== "inner") {
724
+ return "status mode is only available for self/inner dialog.";
639
725
  }
640
- return transcript;
726
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)((0, identity_1.getAgentRoot)());
727
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
728
+ return (0, thoughts_1.formatInnerDialogStatus)((0, thoughts_1.readInnerDialogStatus)(sessionPath, pendingDir));
729
+ }
730
+ const sessFile = (0, config_1.resolveSessionPath)(friendId, channel, key);
731
+ const recall = await recallSessionSafely({
732
+ sessionPath: sessFile,
733
+ friendId,
734
+ channel,
735
+ key,
736
+ messageCount: count,
737
+ trustLevel: ctx?.context?.friend?.trustLevel,
738
+ summarize: ctx?.summarize,
739
+ });
740
+ if (recall.kind === "missing") {
741
+ return NO_SESSION_FOUND_MESSAGE;
641
742
  }
642
- catch {
643
- return "no session found for that friend/channel/key combination.";
743
+ if (recall.kind === "empty") {
744
+ return EMPTY_SESSION_MESSAGE;
644
745
  }
746
+ return recall.summary;
645
747
  },
646
748
  },
647
749
  {
@@ -205,6 +205,8 @@ function summarizeArgs(name, args) {
205
205
  if (name === "save_friend_note") {
206
206
  return summarizeKeyValues(args, ["type", "key", "content"]);
207
207
  }
208
+ if (name === "bridge_manage")
209
+ return summarizeKeyValues(args, ["action", "bridgeId", "objective", "friendId", "channel", "key"]);
208
210
  if (name === "ado_backlog_list")
209
211
  return summarizeKeyValues(args, ["organization", "project"]);
210
212
  if (name === "ado_batch_update")
@@ -604,6 +604,7 @@ async function handleBlueBubblesNormalizedEvent(event, resolvedDeps, source) {
604
604
  try {
605
605
  const result = await (0, pipeline_1.handleInboundTurn)({
606
606
  channel: "bluebubbles",
607
+ sessionKey: event.chat.sessionKey,
607
608
  capabilities: bbCapabilities,
608
609
  messages: [userMessage],
609
610
  continuityIngressTexts: getBlueBubblesContinuityIngressTexts(event),
@@ -752,6 +752,7 @@ async function main(agentName, options) {
752
752
  // User message passed via input.messages so the pipeline can prepend pending messages to it.
753
753
  const result = await (0, pipeline_1.handleInboundTurn)({
754
754
  channel: "cli",
755
+ sessionKey: "session",
755
756
  capabilities: cliCapabilities,
756
757
  messages: [{ role: "user", content: userInput }],
757
758
  continuityIngressTexts: getCliContinuityIngressTexts(userInput),
@@ -4,26 +4,52 @@ exports.createInnerDialogWorker = createInnerDialogWorker;
4
4
  exports.startInnerDialogWorker = startInnerDialogWorker;
5
5
  const inner_dialog_1 = require("./inner-dialog");
6
6
  const runtime_1 = require("../nerves/runtime");
7
- function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options)) {
7
+ const identity_1 = require("../heart/identity");
8
+ const pending_1 = require("../mind/pending");
9
+ function createInnerDialogWorker(runTurn = (options) => (0, inner_dialog_1.runInnerDialogTurn)(options), hasPendingWork = () => (0, pending_1.hasPendingMessages)((0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)()))) {
8
10
  let running = false;
11
+ let rerunRequested = false;
12
+ let rerunReason = "instinct";
13
+ let rerunTaskId;
9
14
  async function run(reason, taskId) {
10
- if (running)
15
+ if (running) {
16
+ rerunRequested = true;
17
+ rerunReason = reason;
18
+ if (taskId !== undefined) {
19
+ rerunTaskId = taskId;
20
+ }
11
21
  return;
22
+ }
12
23
  running = true;
13
24
  try {
14
- await runTurn({ reason, taskId });
15
- }
16
- catch (error) {
17
- (0, runtime_1.emitNervesEvent)({
18
- level: "error",
19
- component: "senses",
20
- event: "senses.inner_dialog_worker_error",
21
- message: "inner dialog worker turn failed",
22
- meta: {
23
- reason,
24
- error: error instanceof Error ? error.message : String(error),
25
- },
26
- });
25
+ let nextReason = reason;
26
+ let nextTaskId = taskId;
27
+ do {
28
+ rerunRequested = false;
29
+ try {
30
+ await runTurn({ reason: nextReason, taskId: nextTaskId });
31
+ }
32
+ catch (error) {
33
+ (0, runtime_1.emitNervesEvent)({
34
+ level: "error",
35
+ component: "senses",
36
+ event: "senses.inner_dialog_worker_error",
37
+ message: "inner dialog worker turn failed",
38
+ meta: {
39
+ reason: nextReason,
40
+ error: error instanceof Error ? error.message : String(error),
41
+ },
42
+ });
43
+ }
44
+ if (!rerunRequested && hasPendingWork()) {
45
+ rerunRequested = true;
46
+ rerunReason = "instinct";
47
+ }
48
+ nextReason = rerunReason;
49
+ nextTaskId = rerunTaskId;
50
+ rerunReason = "instinct";
51
+ rerunTaskId = undefined;
52
+ } while (rerunRequested);
27
53
  }
28
54
  finally {
29
55
  running = false;
@@ -337,6 +337,7 @@ async function runInnerDialogTurn(options) {
337
337
  const traceId = (0, nerves_1.createTraceId)();
338
338
  const result = await (0, pipeline_1.handleInboundTurn)({
339
339
  channel: "inner",
340
+ sessionKey: "dialog",
340
341
  capabilities: innerCapabilities,
341
342
  messages: [userMessage],
342
343
  continuityIngressTexts: [],
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.handleInboundTurn = handleInboundTurn;
9
9
  const runtime_1 = require("../nerves/runtime");
10
10
  const continuity_1 = require("./continuity");
11
+ const manager_1 = require("../heart/bridges/manager");
11
12
  // ── Pipeline ──────────────────────────────────────────────────────
12
13
  async function handleInboundTurn(input) {
13
14
  // Step 1: Resolve friend
@@ -60,6 +61,18 @@ async function handleInboundTurn(input) {
60
61
  ?.map((text) => text.trim())
61
62
  .filter((text) => text.length > 0)
62
63
  .at(-1);
64
+ const currentSession = {
65
+ friendId: resolvedContext.friend.id,
66
+ channel: input.channel,
67
+ key: input.sessionKey ?? "session",
68
+ sessionPath: session.sessionPath,
69
+ };
70
+ const activeBridges = (0, manager_1.createBridgeManager)().findBridgesForSession({
71
+ friendId: currentSession.friendId,
72
+ channel: currentSession.channel,
73
+ key: currentSession.key,
74
+ });
75
+ const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
63
76
  // Step 4: Drain pending messages
64
77
  const pending = input.drainPending(input.pendingDir);
65
78
  // Assemble messages: session messages + pending (formatted) + inbound user messages
@@ -98,6 +111,8 @@ async function handleInboundTurn(input) {
98
111
  const existingToolContext = input.runAgentOptions?.toolContext;
99
112
  const runAgentOptions = {
100
113
  ...input.runAgentOptions,
114
+ bridgeContext,
115
+ currentSessionKey: currentSession.key,
101
116
  currentObligation,
102
117
  mustResolveBeforeHandoff,
103
118
  setMustResolveBeforeHandoff: (value) => {
@@ -109,6 +124,8 @@ async function handleInboundTurn(input) {
109
124
  ...existingToolContext,
110
125
  context: resolvedContext,
111
126
  friendStore: input.friendStore,
127
+ currentSession,
128
+ activeBridges,
112
129
  },
113
130
  };
114
131
  const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
@@ -534,6 +534,7 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
534
534
  // ── Call shared pipeline ──────────────────────────────────────────
535
535
  const result = await (0, pipeline_1.handleInboundTurn)({
536
536
  channel: "teams",
537
+ sessionKey: conversationId,
537
538
  capabilities: teamsCapabilities,
538
539
  messages: [{ role: "user", content: currentText }],
539
540
  continuityIngressTexts: [currentText],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.48",
3
+ "version": "0.1.0-alpha.49",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",