@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.100

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.
Files changed (132) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +147 -205
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +596 -0
  7. package/dist/heart/active-work.js +251 -0
  8. package/dist/heart/bridges/manager.js +358 -0
  9. package/dist/heart/bridges/state-machine.js +135 -0
  10. package/dist/heart/bridges/store.js +123 -0
  11. package/dist/heart/commitments.js +109 -0
  12. package/dist/heart/config.js +102 -23
  13. package/dist/heart/core.js +512 -94
  14. package/dist/heart/cross-chat-delivery.js +146 -0
  15. package/dist/heart/daemon/agent-discovery.js +81 -0
  16. package/dist/heart/daemon/auth-flow.js +430 -0
  17. package/dist/heart/daemon/daemon-cli.js +1935 -185
  18. package/dist/heart/daemon/daemon-entry.js +55 -6
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon.js +218 -9
  21. package/dist/heart/daemon/hatch-animation.js +35 -0
  22. package/dist/heart/daemon/hatch-flow.js +10 -83
  23. package/dist/heart/daemon/hatch-specialist.js +6 -1
  24. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  25. package/dist/heart/daemon/launchd.js +159 -0
  26. package/dist/heart/daemon/log-tailer.js +147 -0
  27. package/dist/heart/daemon/message-router.js +17 -8
  28. package/dist/heart/daemon/os-cron.js +260 -0
  29. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  30. package/dist/heart/daemon/ouro-bot-wrapper.js +4 -3
  31. package/dist/heart/daemon/ouro-path-installer.js +260 -0
  32. package/dist/heart/daemon/ouro-uti.js +11 -2
  33. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  34. package/dist/heart/daemon/process-manager.js +32 -2
  35. package/dist/heart/daemon/run-hooks.js +37 -0
  36. package/dist/heart/daemon/runtime-logging.js +61 -14
  37. package/dist/heart/daemon/runtime-metadata.js +219 -0
  38. package/dist/heart/daemon/runtime-mode.js +67 -0
  39. package/dist/heart/daemon/sense-manager.js +307 -0
  40. package/dist/heart/daemon/skill-management-installer.js +94 -0
  41. package/dist/heart/daemon/socket-client.js +202 -0
  42. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  43. package/dist/heart/daemon/specialist-prompt.js +99 -0
  44. package/dist/heart/daemon/specialist-tools.js +283 -0
  45. package/dist/heart/daemon/staged-restart.js +114 -0
  46. package/dist/heart/daemon/task-scheduler.js +4 -1
  47. package/dist/heart/daemon/thoughts.js +507 -0
  48. package/dist/heart/daemon/update-checker.js +111 -0
  49. package/dist/heart/daemon/update-hooks.js +138 -0
  50. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  51. package/dist/heart/delegation.js +62 -0
  52. package/dist/heart/identity.js +153 -23
  53. package/dist/heart/kicks.js +1 -19
  54. package/dist/heart/model-capabilities.js +48 -0
  55. package/dist/heart/obligations.js +191 -0
  56. package/dist/heart/progress-story.js +42 -0
  57. package/dist/heart/providers/anthropic.js +77 -9
  58. package/dist/heart/providers/azure.js +86 -7
  59. package/dist/heart/providers/github-copilot.js +149 -0
  60. package/dist/heart/providers/minimax.js +4 -0
  61. package/dist/heart/providers/openai-codex.js +12 -3
  62. package/dist/heart/safe-workspace.js +381 -0
  63. package/dist/heart/sense-truth.js +61 -0
  64. package/dist/heart/session-activity.js +169 -0
  65. package/dist/heart/session-recall.js +116 -0
  66. package/dist/heart/streaming.js +103 -22
  67. package/dist/heart/target-resolution.js +123 -0
  68. package/dist/heart/turn-coordinator.js +28 -0
  69. package/dist/mind/associative-recall.js +37 -4
  70. package/dist/mind/bundle-manifest.js +70 -0
  71. package/dist/mind/context.js +141 -11
  72. package/dist/mind/first-impressions.js +16 -2
  73. package/dist/mind/friends/channel.js +43 -0
  74. package/dist/mind/friends/group-context.js +144 -0
  75. package/dist/mind/friends/store-file.js +19 -0
  76. package/dist/mind/friends/trust-explanation.js +74 -0
  77. package/dist/mind/friends/types.js +9 -1
  78. package/dist/mind/memory.js +89 -26
  79. package/dist/mind/obligation-steering.js +31 -0
  80. package/dist/mind/pending.js +160 -0
  81. package/dist/mind/phrases.js +1 -0
  82. package/dist/mind/prompt-refresh.js +20 -0
  83. package/dist/mind/prompt.js +499 -8
  84. package/dist/mind/token-estimate.js +8 -12
  85. package/dist/nerves/cli-logging.js +15 -2
  86. package/dist/nerves/coverage/file-completeness.js +14 -4
  87. package/dist/nerves/coverage/run-artifacts.js +1 -1
  88. package/dist/nerves/index.js +12 -0
  89. package/dist/repertoire/ado-client.js +4 -2
  90. package/dist/repertoire/coding/feedback.js +210 -0
  91. package/dist/repertoire/coding/index.js +4 -1
  92. package/dist/repertoire/coding/manager.js +69 -4
  93. package/dist/repertoire/coding/spawner.js +21 -3
  94. package/dist/repertoire/coding/tools.js +105 -2
  95. package/dist/repertoire/data/ado-endpoints.json +188 -0
  96. package/dist/repertoire/guardrails.js +290 -0
  97. package/dist/repertoire/mcp-client.js +254 -0
  98. package/dist/repertoire/mcp-manager.js +195 -0
  99. package/dist/repertoire/skills.js +3 -26
  100. package/dist/repertoire/tasks/board.js +12 -0
  101. package/dist/repertoire/tasks/index.js +23 -9
  102. package/dist/repertoire/tasks/transitions.js +1 -2
  103. package/dist/repertoire/tools-base.js +770 -213
  104. package/dist/repertoire/tools-bluebubbles.js +93 -0
  105. package/dist/repertoire/tools-teams.js +58 -25
  106. package/dist/repertoire/tools.js +106 -53
  107. package/dist/senses/bluebubbles-client.js +484 -0
  108. package/dist/senses/bluebubbles-entry.js +13 -0
  109. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  110. package/dist/senses/bluebubbles-media.js +339 -0
  111. package/dist/senses/bluebubbles-model.js +261 -0
  112. package/dist/senses/bluebubbles-mutation-log.js +116 -0
  113. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  114. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  115. package/dist/senses/bluebubbles.js +1181 -0
  116. package/dist/senses/cli-layout.js +187 -0
  117. package/dist/senses/cli.js +452 -99
  118. package/dist/senses/continuity.js +94 -0
  119. package/dist/senses/debug-activity.js +154 -0
  120. package/dist/senses/inner-dialog-worker.js +47 -18
  121. package/dist/senses/inner-dialog.js +387 -70
  122. package/dist/senses/pipeline.js +307 -0
  123. package/dist/senses/session-lock.js +119 -0
  124. package/dist/senses/teams.js +574 -129
  125. package/dist/senses/trust-gate.js +112 -2
  126. package/package.json +16 -4
  127. package/subagents/README.md +4 -68
  128. package/dist/heart/daemon/subagent-installer.js +0 -125
  129. package/dist/inner-worker-entry.js +0 -4
  130. package/subagents/work-doer.md +0 -233
  131. package/subagents/work-merger.md +0 -593
  132. package/subagents/work-planner.md +0 -373
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ // Shared per-turn pipeline for all senses.
3
+ // Senses are thin transport adapters; this module owns the common lifecycle:
4
+ // resolve friend -> trust gate -> load session -> drain pending -> runAgent -> postTurn -> token accumulation.
5
+ //
6
+ // Transport-level concerns (BB API calls, Teams cards, readline) stay in sense adapters.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.handleInboundTurn = handleInboundTurn;
9
+ const runtime_1 = require("../nerves/runtime");
10
+ const continuity_1 = require("./continuity");
11
+ const manager_1 = require("../heart/bridges/manager");
12
+ const identity_1 = require("../heart/identity");
13
+ const tasks_1 = require("../repertoire/tasks");
14
+ const session_activity_1 = require("../heart/session-activity");
15
+ const active_work_1 = require("../heart/active-work");
16
+ const delegation_1 = require("../heart/delegation");
17
+ const target_resolution_1 = require("../heart/target-resolution");
18
+ const thoughts_1 = require("../heart/daemon/thoughts");
19
+ const pending_1 = require("../mind/pending");
20
+ const obligations_1 = require("../heart/obligations");
21
+ function emptyTaskBoard() {
22
+ return {
23
+ compact: "",
24
+ full: "",
25
+ byStatus: {
26
+ drafting: [],
27
+ processing: [],
28
+ validating: [],
29
+ collaborating: [],
30
+ paused: [],
31
+ blocked: [],
32
+ done: [],
33
+ },
34
+ actionRequired: [],
35
+ unresolvedDependencies: [],
36
+ activeSessions: [],
37
+ activeBridges: [],
38
+ };
39
+ }
40
+ function readInnerWorkState() {
41
+ const defaultJob = {
42
+ status: "idle",
43
+ content: null,
44
+ origin: null,
45
+ mode: "reflect",
46
+ obligationStatus: null,
47
+ surfacedResult: null,
48
+ queuedAt: null,
49
+ startedAt: null,
50
+ surfacedAt: null,
51
+ };
52
+ try {
53
+ const agentRoot = (0, identity_1.getAgentRoot)();
54
+ const pendingDir = (0, pending_1.getInnerDialogPendingDir)((0, identity_1.getAgentName)());
55
+ const sessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
56
+ const { pendingMessages, turns, runtimeState } = (0, thoughts_1.readInnerDialogRawData)(sessionPath, pendingDir);
57
+ const dialogStatus = (0, thoughts_1.deriveInnerDialogStatus)(pendingMessages, turns, runtimeState);
58
+ const job = (0, thoughts_1.deriveInnerJob)(pendingMessages, turns, runtimeState);
59
+ // Derive obligationPending from both the pending message field and the obligation store
60
+ const storeObligationPending = (0, obligations_1.readPendingObligations)(agentRoot).length > 0;
61
+ return {
62
+ status: dialogStatus.processing === "started" ? "running" : "idle",
63
+ hasPending: dialogStatus.queue !== "clear",
64
+ origin: dialogStatus.origin,
65
+ contentSnippet: dialogStatus.contentSnippet,
66
+ obligationPending: dialogStatus.obligationPending || storeObligationPending,
67
+ job,
68
+ };
69
+ }
70
+ catch {
71
+ return {
72
+ status: "idle",
73
+ hasPending: false,
74
+ job: defaultJob,
75
+ };
76
+ }
77
+ }
78
+ // ── Pipeline ──────────────────────────────────────────────────────
79
+ async function handleInboundTurn(input) {
80
+ // Step 1: Resolve friend
81
+ const resolvedContext = await input.friendResolver.resolve();
82
+ (0, runtime_1.emitNervesEvent)({
83
+ component: "senses",
84
+ event: "senses.pipeline_start",
85
+ message: "inbound turn pipeline started",
86
+ meta: {
87
+ channel: input.channel,
88
+ friendId: resolvedContext.friend.id,
89
+ senseType: input.capabilities.senseType,
90
+ },
91
+ });
92
+ // Step 2: Trust gate
93
+ const gateInput = {
94
+ friend: resolvedContext.friend,
95
+ provider: input.provider ?? "local",
96
+ externalId: input.externalId ?? "",
97
+ tenantId: input.tenantId,
98
+ channel: input.channel,
99
+ senseType: input.capabilities.senseType,
100
+ isGroupChat: input.isGroupChat ?? false,
101
+ groupHasFamilyMember: input.groupHasFamilyMember ?? false,
102
+ hasExistingGroupWithFamily: input.hasExistingGroupWithFamily ?? false,
103
+ };
104
+ const gateResult = input.enforceTrustGate(gateInput);
105
+ // Gate rejection: return early, no agent turn
106
+ if (!gateResult.allowed) {
107
+ (0, runtime_1.emitNervesEvent)({
108
+ component: "senses",
109
+ event: "senses.pipeline_gate_reject",
110
+ message: "trust gate rejected inbound turn",
111
+ meta: {
112
+ channel: input.channel,
113
+ friendId: resolvedContext.friend.id,
114
+ reason: gateResult.reason,
115
+ },
116
+ });
117
+ return {
118
+ resolvedContext,
119
+ gateResult,
120
+ };
121
+ }
122
+ // Step 3: Load/create session
123
+ const session = await input.sessionLoader.loadOrCreate();
124
+ const sessionMessages = session.messages;
125
+ let mustResolveBeforeHandoff = (0, continuity_1.resolveMustResolveBeforeHandoff)(session.state?.mustResolveBeforeHandoff === true, input.continuityIngressTexts);
126
+ const lastFriendActivityAt = input.channel === "inner"
127
+ ? session.state?.lastFriendActivityAt
128
+ : new Date().toISOString();
129
+ const currentObligation = input.continuityIngressTexts
130
+ ?.map((text) => text.trim())
131
+ .filter((text) => text.length > 0)
132
+ .at(-1);
133
+ const currentSession = {
134
+ friendId: resolvedContext.friend.id,
135
+ channel: input.channel,
136
+ key: input.sessionKey ?? "session",
137
+ sessionPath: session.sessionPath,
138
+ };
139
+ const activeBridges = (0, manager_1.createBridgeManager)().findBridgesForSession({
140
+ friendId: currentSession.friendId,
141
+ channel: currentSession.channel,
142
+ key: currentSession.key,
143
+ });
144
+ const bridgeContext = (0, manager_1.formatBridgeContext)(activeBridges) || undefined;
145
+ let sessionActivity = [];
146
+ try {
147
+ const agentRoot = (0, identity_1.getAgentRoot)();
148
+ sessionActivity = (0, session_activity_1.listSessionActivity)({
149
+ sessionsDir: `${agentRoot}/state/sessions`,
150
+ friendsDir: `${agentRoot}/friends`,
151
+ agentName: (0, identity_1.getAgentName)(),
152
+ currentSession: {
153
+ friendId: currentSession.friendId,
154
+ channel: currentSession.channel,
155
+ key: currentSession.key,
156
+ },
157
+ });
158
+ }
159
+ catch {
160
+ sessionActivity = [];
161
+ }
162
+ let targetCandidates = [];
163
+ try {
164
+ if (input.channel !== "inner") {
165
+ const agentRoot = (0, identity_1.getAgentRoot)();
166
+ targetCandidates = await (0, target_resolution_1.listTargetSessionCandidates)({
167
+ sessionsDir: `${agentRoot}/state/sessions`,
168
+ friendsDir: `${agentRoot}/friends`,
169
+ agentName: (0, identity_1.getAgentName)(),
170
+ currentSession: {
171
+ friendId: currentSession.friendId,
172
+ channel: currentSession.channel,
173
+ key: currentSession.key,
174
+ },
175
+ friendStore: input.friendStore,
176
+ });
177
+ }
178
+ }
179
+ catch {
180
+ targetCandidates = [];
181
+ }
182
+ let pendingObligations = [];
183
+ try {
184
+ pendingObligations = (0, obligations_1.readPendingObligations)((0, identity_1.getAgentRoot)());
185
+ }
186
+ catch {
187
+ pendingObligations = [];
188
+ }
189
+ const activeWorkFrame = (0, active_work_1.buildActiveWorkFrame)({
190
+ currentSession,
191
+ currentObligation,
192
+ mustResolveBeforeHandoff,
193
+ inner: readInnerWorkState(),
194
+ bridges: activeBridges,
195
+ pendingObligations,
196
+ taskBoard: (() => {
197
+ try {
198
+ return (0, tasks_1.getTaskModule)().getBoard();
199
+ }
200
+ catch {
201
+ return emptyTaskBoard();
202
+ }
203
+ })(),
204
+ friendActivity: sessionActivity,
205
+ targetCandidates,
206
+ });
207
+ const delegationDecision = (0, delegation_1.decideDelegation)({
208
+ channel: input.channel,
209
+ ingressTexts: input.continuityIngressTexts ?? [],
210
+ activeWork: activeWorkFrame,
211
+ mustResolveBeforeHandoff,
212
+ });
213
+ // Step 4: Drain deferred friend returns, then ordinary per-session pending.
214
+ const deferredReturns = input.channel === "inner"
215
+ ? []
216
+ : (input.drainDeferredReturns?.(resolvedContext.friend.id) ?? []);
217
+ const sessionPending = input.drainPending(input.pendingDir);
218
+ const pending = [...deferredReturns, ...sessionPending];
219
+ // Assemble messages: session messages + pending (formatted) + inbound user messages
220
+ if (pending.length > 0) {
221
+ // Format pending messages and prepend to the user content
222
+ const pendingSection = pending
223
+ .map((msg) => `[pending from ${msg.from}]: ${msg.content}`)
224
+ .join("\n");
225
+ // If there are inbound user messages, prepend pending to the first one
226
+ if (input.messages.length > 0) {
227
+ const firstMsg = input.messages[0];
228
+ if (firstMsg.role === "user") {
229
+ if (typeof firstMsg.content === "string") {
230
+ input.messages[0] = {
231
+ ...firstMsg,
232
+ content: `## pending messages\n${pendingSection}\n\n${firstMsg.content}`,
233
+ };
234
+ }
235
+ else {
236
+ input.messages[0] = {
237
+ ...firstMsg,
238
+ content: [
239
+ { type: "text", text: `## pending messages\n${pendingSection}\n\n` },
240
+ ...firstMsg.content,
241
+ ],
242
+ };
243
+ }
244
+ }
245
+ }
246
+ }
247
+ // Append user messages from the inbound turn
248
+ for (const msg of input.messages) {
249
+ sessionMessages.push(msg);
250
+ }
251
+ // Step 5: runAgent
252
+ const existingToolContext = input.runAgentOptions?.toolContext;
253
+ const runAgentOptions = {
254
+ ...input.runAgentOptions,
255
+ bridgeContext,
256
+ activeWorkFrame,
257
+ delegationDecision,
258
+ currentSessionKey: currentSession.key,
259
+ currentObligation,
260
+ mustResolveBeforeHandoff,
261
+ setMustResolveBeforeHandoff: (value) => {
262
+ mustResolveBeforeHandoff = value;
263
+ },
264
+ toolContext: {
265
+ /* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
266
+ signin: async () => undefined,
267
+ ...existingToolContext,
268
+ context: resolvedContext,
269
+ friendStore: input.friendStore,
270
+ currentSession,
271
+ activeBridges,
272
+ },
273
+ };
274
+ const result = await input.runAgent(sessionMessages, input.callbacks, input.channel, input.signal, runAgentOptions);
275
+ // Step 6: postTurn
276
+ const continuingState = {
277
+ ...(mustResolveBeforeHandoff ? { mustResolveBeforeHandoff: true } : {}),
278
+ ...(typeof lastFriendActivityAt === "string" ? { lastFriendActivityAt } : {}),
279
+ };
280
+ const nextState = result.outcome === "complete" || result.outcome === "blocked" || result.outcome === "superseded" || result.outcome === "no_response"
281
+ ? (typeof lastFriendActivityAt === "string"
282
+ ? { lastFriendActivityAt }
283
+ : undefined)
284
+ : (Object.keys(continuingState).length > 0 ? continuingState : undefined);
285
+ input.postTurn(sessionMessages, session.sessionPath, result.usage, undefined, nextState);
286
+ // Step 7: Token accumulation
287
+ await input.accumulateFriendTokens(input.friendStore, resolvedContext.friend.id, result.usage);
288
+ (0, runtime_1.emitNervesEvent)({
289
+ component: "senses",
290
+ event: "senses.pipeline_end",
291
+ message: "inbound turn pipeline completed",
292
+ meta: {
293
+ channel: input.channel,
294
+ friendId: resolvedContext.friend.id,
295
+ },
296
+ });
297
+ return {
298
+ resolvedContext,
299
+ gateResult,
300
+ usage: result.usage,
301
+ turnOutcome: result.outcome,
302
+ completion: result.completion,
303
+ sessionPath: session.sessionPath,
304
+ messages: sessionMessages,
305
+ drainedPending: pending,
306
+ };
307
+ }
@@ -0,0 +1,119 @@
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.SessionLockError = void 0;
37
+ exports.acquireSessionLock = acquireSessionLock;
38
+ const fs = __importStar(require("fs"));
39
+ const runtime_1 = require("../nerves/runtime");
40
+ class SessionLockError extends Error {
41
+ agentName;
42
+ constructor(agentName) {
43
+ super(`already chatting with ${agentName} in another terminal`);
44
+ this.name = "SessionLockError";
45
+ this.agentName = agentName;
46
+ }
47
+ }
48
+ exports.SessionLockError = SessionLockError;
49
+ function defaultIsProcessAlive(pid) {
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ const defaultDeps = {
59
+ writeFileSync: (p, d, o) => fs.writeFileSync(p, d, o),
60
+ readFileSync: (p, e) => fs.readFileSync(p, e),
61
+ unlinkSync: (p) => fs.unlinkSync(p),
62
+ pid: process.pid,
63
+ isProcessAlive: defaultIsProcessAlive,
64
+ };
65
+ function acquireSessionLock(lockPath, agentName, deps = defaultDeps) {
66
+ const tryWrite = () => {
67
+ try {
68
+ deps.writeFileSync(lockPath, String(deps.pid), { flag: "wx" });
69
+ return true;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ };
75
+ if (!tryWrite()) {
76
+ // Lock file exists — check if stale
77
+ let existingPid;
78
+ try {
79
+ existingPid = parseInt(deps.readFileSync(lockPath, "utf-8").trim(), 10);
80
+ }
81
+ catch {
82
+ // Can't read lock file — try removing and retrying
83
+ try {
84
+ deps.unlinkSync(lockPath);
85
+ }
86
+ catch { /* ignore */ }
87
+ if (!tryWrite())
88
+ throw new SessionLockError(agentName);
89
+ return makeRelease(lockPath, deps);
90
+ }
91
+ if (Number.isNaN(existingPid) || !deps.isProcessAlive(existingPid)) {
92
+ // Stale lock — remove and retry
93
+ try {
94
+ deps.unlinkSync(lockPath);
95
+ }
96
+ catch { /* ignore */ }
97
+ if (!tryWrite())
98
+ throw new SessionLockError(agentName);
99
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.session_lock_stolen", message: "stole stale session lock from dead process", meta: { agentName, existingPid } });
100
+ return makeRelease(lockPath, deps);
101
+ }
102
+ throw new SessionLockError(agentName);
103
+ }
104
+ return makeRelease(lockPath, deps);
105
+ }
106
+ function makeRelease(lockPath, deps) {
107
+ let released = false;
108
+ return {
109
+ release: () => {
110
+ if (released)
111
+ return;
112
+ released = true;
113
+ try {
114
+ deps.unlinkSync(lockPath);
115
+ }
116
+ catch { /* ignore */ }
117
+ },
118
+ };
119
+ }