titan-agent 5.3.0 → 5.3.2

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 (116) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/agent.js +33 -4
  3. package/dist/agent/agent.js.map +1 -1
  4. package/dist/agent/subAgent.js +16 -1
  5. package/dist/agent/subAgent.js.map +1 -1
  6. package/dist/agent/toolRunner.js +17 -0
  7. package/dist/agent/toolRunner.js.map +1 -1
  8. package/dist/config/schema.js +10 -0
  9. package/dist/config/schema.js.map +1 -1
  10. package/dist/eval/record.js +21 -2
  11. package/dist/eval/record.js.map +1 -1
  12. package/dist/gateway/metrics.js +26 -3
  13. package/dist/gateway/metrics.js.map +1 -1
  14. package/dist/gateway/server.js +58 -6
  15. package/dist/gateway/server.js.map +1 -1
  16. package/dist/organism/drives.js +47 -11
  17. package/dist/organism/drives.js.map +1 -1
  18. package/dist/organism/pressure.js +16 -0
  19. package/dist/organism/pressure.js.map +1 -1
  20. package/dist/skills/builtin/fb_autopilot.js +16 -1
  21. package/dist/skills/builtin/fb_autopilot.js.map +1 -1
  22. package/dist/telemetry/activityLog.js +158 -0
  23. package/dist/telemetry/activityLog.js.map +1 -0
  24. package/dist/utils/constants.js +3 -1
  25. package/dist/utils/constants.js.map +1 -1
  26. package/package.json +1 -1
  27. package/ui/dist/assets/{AuditPanel-C31LRHZX.js → AuditPanel-CM6Wg9hO.js} +1 -1
  28. package/ui/dist/assets/{AutonomyPanel-CxQU72ZY.js → AutonomyPanel-CESx3ANg.js} +1 -1
  29. package/ui/dist/assets/{AutopilotPanel-D4FnBwJm.js → AutopilotPanel-DtEet1hJ.js} +1 -1
  30. package/ui/dist/assets/{AutoresearchPanel-BYHXZ9AO.js → AutoresearchPanel-DR47NqT5.js} +1 -1
  31. package/ui/dist/assets/{BackupPanel-C4CQKf2P.js → BackupPanel-BGP8p3l3.js} +1 -1
  32. package/ui/dist/assets/{BrowserPanel-C-OFYyLm.js → BrowserPanel-C15x9bLn.js} +1 -1
  33. package/ui/dist/assets/{CPAgents-CvkZDm_3.js → CPAgents-DYUtPzSq.js} +1 -1
  34. package/ui/dist/assets/{CPDashboard-JmBLBbj7.js → CPDashboard-Bf0-SyCh.js} +1 -1
  35. package/ui/dist/assets/{CPFiles-BDToRw0a.js → CPFiles-CxgxjQcO.js} +1 -1
  36. package/ui/dist/assets/{CPGoals-Dh9qJNWa.js → CPGoals-BsmCMTvT.js} +1 -1
  37. package/ui/dist/assets/{CPInbox-B6iaIbNG.js → CPInbox-tMSbmQ9H.js} +1 -1
  38. package/ui/dist/assets/{CPSocial-CsFrwZRC.js → CPSocial-nb-j7sOE.js} +1 -1
  39. package/ui/dist/assets/{ChannelsPanel-D-S4ktFn.js → ChannelsPanel-DP5C2OKd.js} +1 -1
  40. package/ui/dist/assets/{CheckpointsPanel-D-sP9ZuS.js → CheckpointsPanel-DlranVLZ.js} +1 -1
  41. package/ui/dist/assets/{CommandPostHub-BhlNyeDH.js → CommandPostHub-BgxIa4Ev.js} +3 -3
  42. package/ui/dist/assets/{CronPanel-Bf3rV7N2.js → CronPanel-LoT5yKwJ.js} +1 -1
  43. package/ui/dist/assets/{DaemonPanel-GGBWjTG2.js → DaemonPanel-DBGMqaE_.js} +1 -1
  44. package/ui/dist/assets/{DataTable-D2Px4o6f.js → DataTable-B2Ma8hfi.js} +1 -1
  45. package/ui/dist/assets/{EmptyState-DH6-Jy6A.js → EmptyState-CcKyk5Yn.js} +1 -1
  46. package/ui/dist/assets/EvalHarnessPanel-BqtMc1ZM.js +2 -0
  47. package/ui/dist/assets/{EvalPanel-CdjxzHlJ.js → EvalPanel-Bc33j0pN.js} +1 -1
  48. package/ui/dist/assets/{FilesPanel-Dz8TFydL.js → FilesPanel-3QKvrWPo.js} +1 -1
  49. package/ui/dist/assets/{FleetPanel-CWwWWTD4.js → FleetPanel-CSsXuQYj.js} +1 -1
  50. package/ui/dist/assets/{HomelabPanel-C7VBV7AC.js → HomelabPanel-DhrjTX9m.js} +1 -1
  51. package/ui/dist/assets/{InfraView-B1TgXARJ.js → InfraView-CR6HyrL6.js} +2 -2
  52. package/ui/dist/assets/{InlineEditableField-DOJNOL8m.js → InlineEditableField-CnvF-yFR.js} +1 -1
  53. package/ui/dist/assets/{Input-BgyHgQ3D.js → Input-GTHp2Rkr.js} +1 -1
  54. package/ui/dist/assets/{IntegrationsPanel-O26b4nhv.js → IntegrationsPanel-CymCRE3T.js} +1 -1
  55. package/ui/dist/assets/{IntelligenceView-DUhTQ8f_.js → IntelligenceView-C1IHxJRC.js} +2 -2
  56. package/ui/dist/assets/{LearningPanel-DX5S9ovg.js → LearningPanel-DOCES3lH.js} +1 -1
  57. package/ui/dist/assets/{LogsPanel-DdeTnATQ.js → LogsPanel-BLnAqEaZ.js} +1 -1
  58. package/ui/dist/assets/{McpPanel-BpXWrP1a.js → McpPanel-ChUzmr3z.js} +1 -1
  59. package/ui/dist/assets/{MemoryGraphPanel-CNkZmTUy.js → MemoryGraphPanel-Bzvjmzvk.js} +1 -1
  60. package/ui/dist/assets/{MemoryWikiPanel-o4L8Df-n.js → MemoryWikiPanel-Dwk3Aqwd.js} +1 -1
  61. package/ui/dist/assets/{MeshPanel-DMBQJFCC.js → MeshPanel-C3LJSlht.js} +1 -1
  62. package/ui/dist/assets/{NvidiaPanel-C8P-tJFG.js → NvidiaPanel-CeZK_-CV.js} +1 -1
  63. package/ui/dist/assets/{OrganismPanel-CcfHDWDk.js → OrganismPanel-BB6YOiQV.js} +1 -1
  64. package/ui/dist/assets/{OverviewPanel-BSotI1Zv.js → OverviewPanel-BmtBhQnv.js} +1 -1
  65. package/ui/dist/assets/{PageHeader-DPJuAgJk.js → PageHeader-BimceqQo.js} +1 -1
  66. package/ui/dist/assets/{PaperclipPanel-aXoXUjo6.js → PaperclipPanel-C-brgwA3.js} +1 -1
  67. package/ui/dist/assets/{PersonasPanel-DdPZxz2C.js → PersonasPanel-L1j78p6H.js} +1 -1
  68. package/ui/dist/assets/{RecipesPanel-D7qffXQN.js → RecipesPanel-34lCfynJ.js} +1 -1
  69. package/ui/dist/assets/{SecurityPanel-BDRK5el7.js → SecurityPanel-CBTPWLj6.js} +1 -1
  70. package/ui/dist/assets/{SelfImprovePanel-oYiMwFnA.js → SelfImprovePanel-BrPbFHhG.js} +1 -1
  71. package/ui/dist/assets/{SelfProposalsPanel-DOpNU_rr.js → SelfProposalsPanel-lNmiDThB.js} +1 -1
  72. package/ui/dist/assets/{SessionsPanel-eRbM3D9P.js → SessionsPanel-DAEYIn83.js} +1 -1
  73. package/ui/dist/assets/{SessionsTab-Jq3UKQCI.js → SessionsTab-JQbltWww.js} +1 -1
  74. package/ui/dist/assets/{SettingsPanel-DBIvKUYY.js → SettingsPanel-CzRROAYQ.js} +1 -1
  75. package/ui/dist/assets/{SettingsView-yfSY4OLt.js → SettingsView-CN7ii2uw.js} +2 -2
  76. package/ui/dist/assets/{SkeletonLoader-D1d-Gyyg.js → SkeletonLoader-atQtpcF5.js} +1 -1
  77. package/ui/dist/assets/{SkillsPanel-bubl9nag.js → SkillsPanel-DlFs2ih7.js} +1 -1
  78. package/ui/dist/assets/{SomaView-D3aFL8Tw.js → SomaView-Ba642Oqb.js} +1 -1
  79. package/ui/dist/assets/{StatCard-CEVFsz7t.js → StatCard-DciE_Iqc.js} +1 -1
  80. package/ui/dist/assets/{StatusBadge-DxeA9LNd.js → StatusBadge-BtfSPoW2.js} +1 -1
  81. package/ui/dist/assets/{TeamsPanel-D6IJJIR_.js → TeamsPanel-DKQ5z2Qe.js} +1 -1
  82. package/ui/dist/assets/{TelemetryPanel-SMPebdjQ.js → TelemetryPanel-B6KAc55Q.js} +1 -1
  83. package/ui/dist/assets/{TitanCanvas-BQU1yxqf.js → TitanCanvas-C-s0A-lv.js} +3 -3
  84. package/ui/dist/assets/{ToolsView-DgP4uRPr.js → ToolsView-Dei0KMP0.js} +2 -2
  85. package/ui/dist/assets/{Tooltip-CNPQr7IO.js → Tooltip-70UK0E2I.js} +1 -1
  86. package/ui/dist/assets/{TraceViewer-BbISy_ET.js → TraceViewer-BniolyBx.js} +1 -1
  87. package/ui/dist/assets/{TrainingPanel-BdCHcv6t.js → TrainingPanel-Bz4CTPGW.js} +1 -1
  88. package/ui/dist/assets/{VoiceOverlay-l6yoasVz.js → VoiceOverlay-CmNCrLcd.js} +1 -1
  89. package/ui/dist/assets/{VramPanel-XLhmen92.js → VramPanel-Xh_OtRDR.js} +1 -1
  90. package/ui/dist/assets/{WatchView-Bt-lNNWJ.js → WatchView-C-sGFpVy.js} +1 -1
  91. package/ui/dist/assets/{WorkTab-IG-F6Qll.js → WorkTab-BjLNmgIK.js} +1 -1
  92. package/ui/dist/assets/{WorkflowsPanel-DsMpnwLK.js → WorkflowsPanel-CvgQU1xI.js} +1 -1
  93. package/ui/dist/assets/{arrow-left-C_H9Z2Tm.js → arrow-left-DwqHtJiU.js} +1 -1
  94. package/ui/dist/assets/{chart-column-rR6tb72l.js → chart-column-BtNO6sRy.js} +1 -1
  95. package/ui/dist/assets/{circle-check-big-B1hMwau0.js → circle-check-big-DZRE_MbN.js} +1 -1
  96. package/ui/dist/assets/{dollar-sign-DhYwsTnR.js → dollar-sign-aVG3a5eL.js} +1 -1
  97. package/ui/dist/assets/{download-UDDcAlZC.js → download-BxiWJU4G.js} +1 -1
  98. package/ui/dist/assets/{eye-off-Cx0M_VQb.js → eye-off-CkgfFYhm.js} +1 -1
  99. package/ui/dist/assets/{funnel-B7YvM1ei.js → funnel-PkLdxKyC.js} +1 -1
  100. package/ui/dist/assets/{git-branch-BhTBN3J6.js → git-branch-BM-Gw95X.js} +1 -1
  101. package/ui/dist/assets/{index-D7Clon2u.js → index-CahJbWSR.js} +2 -2
  102. package/ui/dist/assets/{layers-B6jDzitD.js → layers-BuGf4FIJ.js} +1 -1
  103. package/ui/dist/assets/{legacy-av079XKu.js → legacy-CR6o4t-y.js} +1 -1
  104. package/ui/dist/assets/{lightbulb-DRuQ3Chf.js → lightbulb-n8gc_XAL.js} +1 -1
  105. package/ui/dist/assets/{pause-DqkRWPB_.js → pause-DCV52koX.js} +1 -1
  106. package/ui/dist/assets/{play-hUyR3CVS.js → play-CcJ9BnCh.js} +1 -1
  107. package/ui/dist/assets/{plug-CvpyjJt_.js → plug-CfWBXfCl.js} +1 -1
  108. package/ui/dist/assets/{proxy-Cc5bR828.js → proxy-CzZDfLmm.js} +1 -1
  109. package/ui/dist/assets/{square-CdiC0J8Z.js → square-DJpUhlxi.js} +1 -1
  110. package/ui/dist/assets/{target-DemL8_0v.js → target-DWcmM_9m.js} +1 -1
  111. package/ui/dist/assets/{toggle-right-Dsk892k5.js → toggle-right-YusFQ69L.js} +1 -1
  112. package/ui/dist/assets/{trash-2-Byj4OvKB.js → trash-2-CK7yQ55V.js} +1 -1
  113. package/ui/dist/assets/{trending-up-Dh_CffGX.js → trending-up-DGjFyubC.js} +1 -1
  114. package/ui/dist/assets/{trophy-DDr2AePx.js → trophy-uQv_NgDB.js} +1 -1
  115. package/ui/dist/index.html +1 -1
  116. package/ui/dist/assets/EvalHarnessPanel-Cz9dRg61.js +0 -2
@@ -198,24 +198,46 @@ const SOCIAL = {
198
198
  weight: 0.7,
199
199
  compute: (snap) => {
200
200
  const eligible = snap.agents.filter((a) => (a.totalTasksCompleted ?? 0) > 0 || a.status === "active");
201
- if (eligible.length === 0) {
202
- return { satisfaction: 0.9, inputs: { totalAgents: snap.agents.length, staleAgents: 0 } };
203
- }
204
201
  const hourMs = 36e5;
205
- const stale = eligible.filter(
206
- (a) => snap.now - new Date(a.lastHeartbeat).getTime() > hourMs
207
- ).length;
208
- const satisfaction = clamp01(1 - stale / eligible.length);
202
+ let agentSat = 0.9;
203
+ let stale = 0;
204
+ if (eligible.length > 0) {
205
+ stale = eligible.filter(
206
+ (a) => snap.now - new Date(a.lastHeartbeat).getTime() > hourMs
207
+ ).length;
208
+ agentSat = clamp01(1 - stale / eligible.length);
209
+ }
210
+ const POST_DROUGHT_HOURS = 24;
211
+ let postSat;
212
+ let hoursSinceLastPost;
213
+ if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {
214
+ hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);
215
+ postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);
216
+ } else {
217
+ hoursSinceLastPost = POST_DROUGHT_HOURS / 2;
218
+ postSat = 0.5;
219
+ }
220
+ const satisfaction = clamp01((agentSat + postSat) / 2);
209
221
  return {
210
222
  satisfaction,
211
- inputs: { totalAgents: eligible.length, staleAgents: stale }
223
+ inputs: {
224
+ totalAgents: eligible.length,
225
+ staleAgents: stale,
226
+ hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),
227
+ agentSatisfaction: Number(agentSat.toFixed(3)),
228
+ postSatisfaction: Number(postSat.toFixed(3))
229
+ }
212
230
  };
213
231
  },
214
232
  describe: (_s, inputs) => {
215
233
  const total = inputs?.totalAgents ?? 0;
216
234
  const stale = inputs?.staleAgents ?? 0;
217
- if (stale === 0) return `${total} agent(s) all alive`;
218
- return `${stale}/${total} agent(s) unresponsive`;
235
+ const hoursSince = inputs?.hoursSinceLastPost ?? 0;
236
+ const reasons = [];
237
+ if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);
238
+ if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);
239
+ if (reasons.length === 0) return `${total} agent(s) all alive \xB7 posted recently`;
240
+ return reasons.join(" \xB7 ");
219
241
  }
220
242
  };
221
243
  const DRIVES = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];
@@ -260,6 +282,19 @@ function buildSnapshot() {
260
282
  if (patterns !== null) unresolvedErrorPatterns = patterns;
261
283
  } catch {
262
284
  }
285
+ let lastFacebookPostAt = null;
286
+ try {
287
+ const fbStatePath = join(TITAN_HOME, "fb-autopilot-state.json");
288
+ if (existsSync(fbStatePath)) {
289
+ const raw = readFileSync(fbStatePath, "utf-8");
290
+ const state = JSON.parse(raw);
291
+ if (state.lastPostAt) {
292
+ const parsed = new Date(state.lastPostAt).getTime();
293
+ if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;
294
+ }
295
+ }
296
+ } catch {
297
+ }
263
298
  return {
264
299
  now: Date.now(),
265
300
  goals,
@@ -271,7 +306,8 @@ function buildSnapshot() {
271
306
  vramSaturation,
272
307
  telemetryErrorRate,
273
308
  telemetryTotalRequests,
274
- unresolvedErrorPatterns
309
+ unresolvedErrorPatterns,
310
+ lastFacebookPostAt
275
311
  };
276
312
  }
277
313
  function readCachedVRAMSignal() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n if (eligible.length === 0) {\n return { satisfaction: 0.9, inputs: { totalAgents: snap.agents.length, staleAgents: 0 } };\n }\n const hourMs = 3_600_000;\n const stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n const satisfaction = clamp01(1 - stale / eligible.length);\n return {\n satisfaction,\n inputs: { totalAgents: eligible.length, staleAgents: stale },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n if (stale === 0) return `${total} agent(s) all alive`;\n return `${stale}/${total} agent(s) unresponsive`;\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n ensureDir(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/organism/drives.ts"],"sourcesContent":["/**\n * TITAN — Drive Layer (Soma organism / endocrine system)\n *\n * Five homeostatic drives. Each computes a 0-1 \"satisfaction\" from existing\n * TITAN telemetry — no new instrumentation. When satisfaction dips below the\n * drive's setpoint, pressure accumulates. Cross-drive pressure fusion (see\n * pressure.ts) eventually produces a soma_proposal for human approval.\n *\n * Gated by config.organism.enabled — this module is inert when disabled.\n *\n * DRIVES SHIPPED IN v4.0:\n * Purpose — alignment with priority-1 goals\n * Hunger — backlog size vs. throughput\n * Curiosity — task-type diversity in recent trajectories\n * Safety — budget runway + recent error rate\n * Social — stale agent fraction\n *\n * DEFERRED TO v4.1+:\n * Hygiene — needs npm test + git status shell hooks\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { ensureDir } from '../utils/helpers.js';\nimport { listGoals, getReadyTasks, type Goal } from '../agent/goals.js';\nimport { getRegisteredAgents, getBudgetPolicies, listRuns, type RegisteredAgent, type BudgetPolicy, type CPRun } from '../agent/commandPost.js';\nimport { getRecentTrajectories, type TaskTrajectory } from '../agent/trajectoryLogger.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Drives';\nconst DRIVE_STATE_PATH = join(TITAN_HOME, 'drive-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type DriveId = 'purpose' | 'hunger' | 'curiosity' | 'safety' | 'social';\n\nexport interface DriveSnapshot {\n /** Timestamp of the snapshot in epoch ms. */\n now: number;\n /** All goals from goals.ts. */\n goals: Goal[];\n /** Output of getReadyTasks() — ready-to-execute subtasks. */\n readyTasks: Array<{ goal: Goal; subtask: Goal['subtasks'][number] }>;\n /** Recent CPRun history (up to 100 most recent). */\n recentRuns: CPRun[];\n /** Active budget policies. */\n budgets: BudgetPolicy[];\n /** All registered agents. */\n agents: RegisteredAgent[];\n /** Last 100 trajectory entries. */\n trajectories: TaskTrajectory[];\n /**\n * v4.9.0: fraction of GPU VRAM in use (0–1). Undefined when no GPU\n * is attached or the orchestrator hasn't refreshed yet.\n */\n vramSaturation?: number;\n /**\n * v4.9.0: error rate across recent LLM / tool calls from the\n * gateway metrics layer (0–1). Undefined when metrics are unavailable.\n */\n telemetryErrorRate?: number;\n /** v4.9.0: total LLM + tool-call requests since gateway start. */\n telemetryTotalRequests?: number;\n /**\n * v4.9.0: count of error patterns the learning layer has accumulated\n * but not yet resolved. High count pulls Curiosity toward an\n * investigate/improve proposal.\n */\n unresolvedErrorPatterns?: number;\n /**\n * v5.3.2 (Phase 8 / Track B): timestamp of the most recent successful\n * Facebook page post in epoch ms, or null if TITAN hasn't posted yet\n * since fb-autopilot was enabled. Sourced from\n * `~/.titan/fb-autopilot-state.json`. Drives the Social-drive's\n * \"social media presence\" factor — without this, social pressure was\n * 100% about agent heartbeat staleness, which the README implied was\n * about social posting. Now both factors blend.\n */\n lastFacebookPostAt?: number | null;\n}\n\nexport interface DriveDefinition {\n id: DriveId;\n label: string;\n /** Satisfaction level below which this drive starts contributing pressure. */\n defaultSetpoint: number;\n /** Relative weight in cross-drive pressure fusion (1.0 is baseline). */\n weight: number;\n /** Pure function — computes satisfaction 0-1 from the snapshot. */\n compute: (snapshot: DriveSnapshot) => { satisfaction: number; inputs?: Record<string, unknown> };\n /** Short human-readable explanation used in prompts, UI tooltips, and activity feed. */\n describe: (satisfaction: number, inputs?: Record<string, unknown>) => string;\n}\n\nexport interface DriveState {\n id: DriveId;\n label: string;\n satisfaction: number;\n setpoint: number;\n /** 0 when satisfaction >= setpoint, else (setpoint − satisfaction) × weight. */\n pressure: number;\n weight: number;\n inputs?: Record<string, unknown>;\n description: string;\n}\n\nexport interface DriveTickResult {\n timestamp: string;\n drives: DriveState[];\n totalPressure: number;\n dominantDrives: DriveId[];\n}\n\n// ── Numeric helpers ──────────────────────────────────────────────\n\n/** Clamp to [0,1]. */\nfunction clamp01(v: number): number {\n if (!Number.isFinite(v)) return 0;\n return Math.max(0, Math.min(1, v));\n}\n\n/** Sigmoid centred on `mid` with slope `k`. Returns high → 1 when x is low. */\nfunction invertedSigmoid(x: number, mid: number, k = 1): number {\n return clamp01(1 / (1 + Math.exp(k * (x - mid))));\n}\n\n/** Gini coefficient of a count distribution. 0 = uniform, 1 = all same task. */\nfunction gini(counts: number[]): number {\n if (counts.length === 0) return 0;\n const n = counts.length;\n const sum = counts.reduce((a, b) => a + b, 0);\n if (sum === 0) return 0;\n const sorted = [...counts].sort((a, b) => a - b);\n let cum = 0;\n for (let i = 0; i < n; i++) cum += (i + 1) * sorted[i];\n return clamp01((2 * cum) / (n * sum) - (n + 1) / n);\n}\n\n// ── Drive definitions ────────────────────────────────────────────\n\nconst PURPOSE: DriveDefinition = {\n id: 'purpose',\n label: 'Purpose',\n defaultSetpoint: 0.7,\n weight: 1.4,\n compute: (snap) => {\n // Priority-1 goals tagged as high-priority. Satisfaction reflects how\n // recently any of them progressed. No priority-1 goals → satiated\n // (nothing to worry about).\n const priorityOne = snap.goals.filter(g =>\n g.status === 'active' && g.priority === 1,\n );\n if (priorityOne.length === 0) {\n return { satisfaction: 0.9, inputs: { priorityOneCount: 0 } };\n }\n const latest = Math.max(...priorityOne.map(g =>\n new Date(g.updatedAt || g.createdAt).getTime(),\n ));\n const hoursSince = Math.max(0, (snap.now - latest) / 3_600_000);\n const satisfaction = clamp01(1 - hoursSince / 24);\n return {\n satisfaction,\n inputs: { priorityOneCount: priorityOne.length, hoursSinceProgress: Math.round(hoursSince * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.priorityOneCount as number) ?? 0;\n if (count === 0) return 'no priority-1 goals in flight';\n const hours = (inputs?.hoursSinceProgress as number) ?? 0;\n if (s < 0.3) return `${count} priority-1 goal(s) stalled — no progress in ${hours.toFixed(1)}h`;\n if (s < 0.6) return `${count} priority-1 goal(s) need attention`;\n return `${count} priority-1 goal(s) on track`;\n },\n};\n\nconst HUNGER: DriveDefinition = {\n id: 'hunger',\n label: 'Hunger',\n defaultSetpoint: 0.6,\n weight: 1.0,\n compute: (snap) => {\n const readyCount = snap.readyTasks.length;\n // Oldest ready subtask age in hours, using parent goal createdAt as proxy.\n const oldestAgeHours = snap.readyTasks.length === 0\n ? 0\n : Math.max(...snap.readyTasks.map(r =>\n (snap.now - new Date(r.goal.createdAt).getTime()) / 3_600_000,\n ));\n // Both signals independently drag satisfaction down.\n // v5.0.0: floor backlog satisfaction at 0.15 so extreme backlogs\n // (e.g. 1000+ zombie goals) don't drive hunger to absolute zero,\n // which causes SOMA to panic-propose even more goals.\n const backlogSatisfaction = Math.max(0.15, invertedSigmoid(readyCount, 5, 0.35));\n const ageSatisfaction = invertedSigmoid(oldestAgeHours, 4, 0.5);\n const satisfaction = Math.min(backlogSatisfaction, ageSatisfaction);\n return {\n satisfaction,\n inputs: { readyCount, oldestAgeHours: Math.round(oldestAgeHours * 10) / 10 },\n };\n },\n describe: (s, inputs) => {\n const count = (inputs?.readyCount as number) ?? 0;\n const age = (inputs?.oldestAgeHours as number) ?? 0;\n if (count === 0) return 'backlog empty';\n if (s < 0.3) return `backlog ${count}, oldest ${age.toFixed(1)}h — elevated`;\n if (s < 0.6) return `backlog ${count}, oldest ${age.toFixed(1)}h`;\n return `backlog ${count} — fed`;\n },\n};\n\nconst CURIOSITY: DriveDefinition = {\n id: 'curiosity',\n label: 'Curiosity',\n defaultSetpoint: 0.5,\n weight: 0.8,\n compute: (snap) => {\n // Novelty = task-type diversity across recent trajectories.\n // Few distinct task types → elevated curiosity (stale). Rich variety\n // → satiated. We compose two signals:\n // 1) coverage: how many distinct types relative to a target of 5\n // 2) balance: how evenly distributed those types are (1 − gini)\n // Satisfaction = min(coverage, balance) so either deficit pulls it\n // down. Low sample counts default to middling satisfaction.\n if (snap.trajectories.length < 5) {\n return { satisfaction: 0.6, inputs: { trajectoryCount: snap.trajectories.length } };\n }\n const typeCounts: Record<string, number> = {};\n for (const t of snap.trajectories) {\n typeCounts[t.taskType || 'unknown'] = (typeCounts[t.taskType || 'unknown'] || 0) + 1;\n }\n const typeCount = Object.keys(typeCounts).length;\n const coverage = clamp01(typeCount / 5);\n const counts = Object.values(typeCounts);\n const balance = typeCount <= 1 ? 0 : clamp01(1 - gini(counts));\n const diversitySat = typeCount <= 1 ? coverage : Math.min(coverage, balance);\n\n // v4.9.0: unresolved error patterns are a form of \"task-type\n // novelty the organism hasn't figured out yet.\" More than a\n // handful of unresolved patterns pulls Curiosity toward an\n // investigate-and-improve proposal (feeds Self-Improve pipeline).\n // Scales 0→10+ patterns linearly.\n let errorPatternSat = 1;\n if (typeof snap.unresolvedErrorPatterns === 'number' && snap.unresolvedErrorPatterns > 2) {\n errorPatternSat = clamp01(1 - (snap.unresolvedErrorPatterns - 2) / 10);\n }\n\n const satisfaction = Math.min(diversitySat, errorPatternSat);\n return {\n satisfaction,\n inputs: {\n trajectoryCount: snap.trajectories.length,\n taskTypes: typeCount,\n coverage: Math.round(coverage * 100) / 100,\n balance: Math.round(balance * 100) / 100,\n unresolvedErrorPatterns: snap.unresolvedErrorPatterns ?? 0,\n errorPatternSat: Math.round(errorPatternSat * 100) / 100,\n },\n };\n },\n describe: (s, inputs) => {\n const types = (inputs?.taskTypes as number) ?? 0;\n const patterns = (inputs?.unresolvedErrorPatterns as number) ?? 0;\n if (patterns >= 5) return `${patterns} unresolved error patterns — needs investigation`;\n if (s < 0.3) return `stuck in ${types} task type(s) — stale`;\n if (s < 0.6) return `${types} task type(s) — could use novelty`;\n return `${types} distinct task type(s) — engaged`;\n },\n};\n\nconst SAFETY: DriveDefinition = {\n id: 'safety',\n label: 'Safety',\n defaultSetpoint: 0.8,\n weight: 1.6,\n compute: (snap) => {\n // Budget runway: min runway across all enabled budgets.\n let budgetSatisfaction = 1;\n const relevantBudgets = snap.budgets.filter(b => b.enabled && b.limitUsd > 0);\n if (relevantBudgets.length > 0) {\n const runways = relevantBudgets.map(b => clamp01(1 - b.currentSpend / b.limitUsd));\n budgetSatisfaction = Math.min(...runways);\n }\n // Recent error rate from last 100 CPRuns in the last 24h.\n const dayMs = 86_400_000;\n const recent = snap.recentRuns.filter(r =>\n snap.now - new Date(r.startedAt).getTime() < dayMs,\n );\n let errorSatisfaction = 1;\n if (recent.length >= 5) {\n const errors = recent.filter(r => r.status === 'error' || r.status === 'failed').length;\n errorSatisfaction = clamp01(1 - errors / recent.length);\n }\n\n // v4.9.0: VRAM saturation above 85% presses Safety. Below 85%,\n // saturation has no effect. Scales linearly 85%–100% → sat 1→0.\n let vramSatisfaction = 1;\n if (snap.vramSaturation !== undefined) {\n if (snap.vramSaturation > 0.85) {\n vramSatisfaction = clamp01(1 - (snap.vramSaturation - 0.85) / 0.15);\n }\n }\n\n // v4.9.0: gateway-level telemetry error rate (LLM/tool calls).\n // Independent of CPRun error rate — catches tool failures that\n // never bubbled up to Command Post.\n let telemetrySatisfaction = 1;\n if (snap.telemetryErrorRate !== undefined) {\n telemetrySatisfaction = clamp01(1 - snap.telemetryErrorRate * 2);\n }\n\n // Safety is a min-aggregate — the weakest link dominates.\n const satisfaction = Math.min(\n budgetSatisfaction,\n errorSatisfaction,\n vramSatisfaction,\n telemetrySatisfaction,\n );\n return {\n satisfaction,\n inputs: {\n budgetSatisfaction: Math.round(budgetSatisfaction * 100) / 100,\n errorSatisfaction: Math.round(errorSatisfaction * 100) / 100,\n vramSatisfaction: Math.round(vramSatisfaction * 100) / 100,\n telemetrySatisfaction: Math.round(telemetrySatisfaction * 100) / 100,\n recentRunCount: recent.length,\n vramSaturationPct: snap.vramSaturation !== undefined ? Math.round(snap.vramSaturation * 100) : null,\n telemetryErrorRatePct: snap.telemetryErrorRate !== undefined ? Math.round(snap.telemetryErrorRate * 100) : null,\n },\n };\n },\n describe: (s, inputs) => {\n const budget = (inputs?.budgetSatisfaction as number) ?? 1;\n const errors = (inputs?.errorSatisfaction as number) ?? 1;\n const vram = (inputs?.vramSatisfaction as number) ?? 1;\n const tel = (inputs?.telemetrySatisfaction as number) ?? 1;\n if (budget < 0.2) return 'budget runway critical';\n if (vram < 0.4) return `VRAM saturated (${inputs?.vramSaturationPct}%) — spawns at risk`;\n if (tel < 0.5) return `gateway error rate elevated (${inputs?.telemetryErrorRatePct}%)`;\n if (errors < 0.5) return 'elevated error rate in recent runs';\n if (s < 0.6) return 'safety posture weakening';\n return 'safety posture healthy';\n },\n};\n\nconst SOCIAL: DriveDefinition = {\n id: 'social',\n label: 'Social',\n defaultSetpoint: 0.7,\n weight: 0.7,\n compute: (snap) => {\n // v4.8.1: ignore specialists that were registered but never given\n // work (`totalTasksCompleted === 0`). They have nothing to heartbeat\n // about; counting them as \"unresponsive\" was a false negative.\n const eligible = snap.agents.filter(a => (a.totalTasksCompleted ?? 0) > 0 || a.status === 'active');\n const hourMs = 3_600_000;\n\n // ── Factor 1: agent liveness (legacy) ───────────────────────\n let agentSat = 0.9; // healthy default when no eligible agents\n let stale = 0;\n if (eligible.length > 0) {\n stale = eligible.filter(a =>\n snap.now - new Date(a.lastHeartbeat).getTime() > hourMs,\n ).length;\n agentSat = clamp01(1 - stale / eligible.length);\n }\n\n // ── Factor 2: social-media presence (v5.3.2) ────────────────\n // The README promises a Social drive that asks \"should I post or\n // reply?\" — that requires the drive to actually track posting\n // cadence, not just agent heartbeat. lastFacebookPostAt is wired\n // through buildSnapshot from fb-autopilot-state.json.\n //\n // Saturates at 24h: a drought of 24h+ with no post pulls\n // satisfaction to 0; a fresh post within the last hour keeps it\n // near 1. Linear in between. If lastFacebookPostAt is null/missing\n // (autopilot never ran or never posted), we treat the gap as\n // \"long\" — encourages a first post when a user enables FB.\n const POST_DROUGHT_HOURS = 24;\n let postSat: number;\n let hoursSinceLastPost: number;\n if (snap.lastFacebookPostAt && snap.lastFacebookPostAt > 0) {\n hoursSinceLastPost = Math.max(0, (snap.now - snap.lastFacebookPostAt) / hourMs);\n postSat = clamp01(1 - hoursSinceLastPost / POST_DROUGHT_HOURS);\n } else {\n // Treat \"never posted\" as ~12h drought. Don't peg to 0 —\n // organism shouldn't fire a Soma proposal the moment FB is\n // enabled before the user has even configured anything.\n hoursSinceLastPost = POST_DROUGHT_HOURS / 2;\n postSat = 0.5;\n }\n\n // Equal-weight blend. Either factor low → drive deficits.\n const satisfaction = clamp01((agentSat + postSat) / 2);\n\n return {\n satisfaction,\n inputs: {\n totalAgents: eligible.length,\n staleAgents: stale,\n hoursSinceLastPost: Number(hoursSinceLastPost.toFixed(2)),\n agentSatisfaction: Number(agentSat.toFixed(3)),\n postSatisfaction: Number(postSat.toFixed(3)),\n },\n };\n },\n describe: (_s, inputs) => {\n const total = (inputs?.totalAgents as number) ?? 0;\n const stale = (inputs?.staleAgents as number) ?? 0;\n const hoursSince = (inputs?.hoursSinceLastPost as number) ?? 0;\n const reasons: string[] = [];\n if (stale > 0) reasons.push(`${stale}/${total} agent(s) unresponsive`);\n if (hoursSince >= 12) reasons.push(`${Math.round(hoursSince)}h since last FB post`);\n if (reasons.length === 0) return `${total} agent(s) all alive · posted recently`;\n return reasons.join(' · ');\n },\n};\n\nexport const DRIVES: DriveDefinition[] = [PURPOSE, HUNGER, CURIOSITY, SAFETY, SOCIAL];\n\n// ── Snapshot builder ─────────────────────────────────────────────\n\n/** Build a DriveSnapshot by reading current TITAN state. Synchronous —\n * all inputs are in-memory or cheap disk reads. */\nexport function buildSnapshot(): DriveSnapshot {\n const goals = listGoals();\n let readyTasks: DriveSnapshot['readyTasks'] = [];\n try { readyTasks = getReadyTasks(); } catch { /* empty */ }\n const agents = getRegisteredAgents();\n const budgets = getBudgetPolicies();\n let recentRuns: CPRun[] = [];\n try { recentRuns = listRuns(undefined, 100); } catch { /* empty */ }\n let trajectories: TaskTrajectory[] = [];\n try { trajectories = getRecentTrajectories(100); } catch { /* empty */ }\n\n // v4.9.0 — pull optional closed-loop signals. Each wrapped in try so\n // drive tick never fails if a downstream module is missing or throws.\n\n let vramSaturation: number | undefined;\n try {\n const vr = readCachedVRAMSignal();\n if (vr !== null) vramSaturation = vr;\n } catch { /* no signal */ }\n\n let telemetryErrorRate: number | undefined;\n let telemetryTotalRequests: number | undefined;\n try {\n const metrics = readCachedTelemetrySignal();\n if (metrics) {\n telemetryErrorRate = metrics.errorRate;\n telemetryTotalRequests = metrics.totalRequests;\n }\n } catch { /* no signal */ }\n\n let unresolvedErrorPatterns: number | undefined;\n try {\n const patterns = readUnresolvedErrorPatternCount();\n if (patterns !== null) unresolvedErrorPatterns = patterns;\n } catch { /* no signal */ }\n\n // v5.3.2 Track B: read fb-autopilot's last successful post timestamp so\n // the Social drive's \"social media presence\" factor has real input.\n // Best-effort: never throws — Social drive falls back to a neutral\n // \"12h drought\" when this is absent (see SOCIAL.compute).\n let lastFacebookPostAt: number | null = null;\n try {\n const fbStatePath = join(TITAN_HOME, 'fb-autopilot-state.json');\n if (existsSync(fbStatePath)) {\n const raw = readFileSync(fbStatePath, 'utf-8');\n const state = JSON.parse(raw) as { lastPostAt?: string | null };\n if (state.lastPostAt) {\n const parsed = new Date(state.lastPostAt).getTime();\n if (Number.isFinite(parsed)) lastFacebookPostAt = parsed;\n }\n }\n } catch { /* ok — autopilot state missing or malformed; fall back */ }\n\n return {\n now: Date.now(),\n goals,\n readyTasks,\n recentRuns,\n budgets,\n agents,\n trajectories,\n vramSaturation,\n telemetryErrorRate,\n telemetryTotalRequests,\n unresolvedErrorPatterns,\n lastFacebookPostAt,\n };\n}\n\n// ── v4.9.0 signal readers ──────────────────────────────────────────\n\n/**\n * Reads the VRAM orchestrator's last cached snapshot (no refresh) and\n * returns used/total saturation as 0–1. Returns null when no GPU is\n * attached or the orchestrator hasn't polled yet.\n *\n * Synchronous: buildSnapshot() is called in the drive-tick hot path\n * every 60s, and we don't want to add an async nvidia-smi probe on\n * top of the existing 10s VRAM refresh.\n */\nfunction readCachedVRAMSignal(): number | null {\n try {\n // Dynamic require-like import from the already-loaded module\n // singleton. If VRAM module hasn't been initialized (e.g., in\n // tests), just return null.\n const mod = (globalThis as unknown as { __titan_vram_last?: { freeMB?: number; totalMB?: number; usedMB?: number } }).__titan_vram_last;\n if (!mod) return null;\n const total = mod.totalMB ?? 0;\n if (!Number.isFinite(total) || total <= 0) return null;\n const used = Number.isFinite(mod.usedMB) ? mod.usedMB! : (total - (mod.freeMB ?? total));\n const pct = used / total;\n if (!Number.isFinite(pct)) return null;\n return Math.max(0, Math.min(1, pct));\n } catch {\n return null;\n }\n}\n\n/** Reads the gateway metrics layer's summary (sync, in-memory). */\nfunction readCachedTelemetrySignal(): { errorRate: number; totalRequests: number } | null {\n try {\n // Using require-style resolve so tests that mock the drives\n // module don't pull in the metrics graph.\n const mod = (globalThis as unknown as { __titan_metrics_summary?: () => { totalRequests?: number; errorRate?: number } | null }).__titan_metrics_summary;\n if (typeof mod !== 'function') return null;\n const s = mod();\n if (!s || typeof s.totalRequests !== 'number' || typeof s.errorRate !== 'number') return null;\n // Only treat the signal as meaningful once we have enough samples.\n if (s.totalRequests < 10) return null;\n return { errorRate: s.errorRate, totalRequests: s.totalRequests };\n } catch {\n return null;\n }\n}\n\n/** Reads count of unresolved error patterns from the learning layer. */\nfunction readUnresolvedErrorPatternCount(): number | null {\n try {\n const mod = (globalThis as unknown as { __titan_unresolved_error_patterns?: () => number }).__titan_unresolved_error_patterns;\n if (typeof mod !== 'function') return null;\n const n = mod();\n if (typeof n !== 'number' || !Number.isFinite(n)) return null;\n return n;\n } catch {\n return null;\n }\n}\n\n// ── Drive state computation ──────────────────────────────────────\n\n/** Compute all drive states for a given snapshot, applying per-drive\n * setpoint + weight overrides + disabled-drive filter (all from\n * config.organism.{driveSetpoints,driveWeights,disabledDrives}). */\nexport function computeAllDrives(\n snapshot: DriveSnapshot,\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveState[] {\n const out: DriveState[] = [];\n const disabled = new Set(disabledDrives);\n for (const def of DRIVES) {\n if (disabled.has(def.id)) continue;\n const { satisfaction, inputs } = def.compute(snapshot);\n const setpoint = setpointOverrides[def.id] ?? def.defaultSetpoint;\n const weight = weightOverrides[def.id] ?? def.weight;\n const pressure = satisfaction < setpoint\n ? (setpoint - satisfaction) * weight\n : 0;\n out.push({\n id: def.id,\n label: def.label,\n satisfaction: clamp01(satisfaction),\n setpoint,\n pressure,\n weight,\n inputs,\n description: def.describe(satisfaction, inputs),\n });\n }\n return out;\n}\n\n// ── Persistence ──────────────────────────────────────────────────\n\nexport interface PersistedDriveHistory {\n latest: DriveTickResult;\n /** Ring buffer of last ≤1440 ticks (~24h at 60s cadence). */\n history: Array<{ timestamp: string; satisfactions: Record<DriveId, number> }>;\n}\n\n/** Load the last-written drive state (if any). Returns null on first run. */\nexport function loadDriveHistory(): PersistedDriveHistory | null {\n if (!existsSync(DRIVE_STATE_PATH)) return null;\n try {\n return JSON.parse(readFileSync(DRIVE_STATE_PATH, 'utf-8')) as PersistedDriveHistory;\n } catch (err) {\n logger.warn(COMPONENT, `drive-state.json corrupt: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Persist the tick. Ring-buffers history to a max of 1440 entries. */\nexport function saveDriveTick(tick: DriveTickResult): void {\n try {\n ensureDir(TITAN_HOME);\n const existing = loadDriveHistory();\n const satisfactions: Record<string, number> = {};\n for (const d of tick.drives) satisfactions[d.id] = d.satisfaction;\n const history = (existing?.history || []).concat([{\n timestamp: tick.timestamp,\n satisfactions: satisfactions as Record<DriveId, number>,\n }]);\n const trimmed = history.length > 1440 ? history.slice(-1440) : history;\n const payload: PersistedDriveHistory = { latest: tick, history: trimmed };\n writeFileSync(DRIVE_STATE_PATH, JSON.stringify(payload, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save drive state: ${(err as Error).message}`);\n }\n}\n\n// ── One-call convenience ─────────────────────────────────────────\n\n/** Build snapshot → compute drives → package as a DriveTickResult. Does NOT\n * persist; callers decide whether to save (daemon tick does; read-only API\n * endpoints don't). */\nexport function runDriveTick(\n setpointOverrides: Partial<Record<DriveId, number>> = {},\n weightOverrides: Partial<Record<DriveId, number>> = {},\n disabledDrives: DriveId[] = [],\n): DriveTickResult {\n const snapshot = buildSnapshot();\n const drives = computeAllDrives(snapshot, setpointOverrides, weightOverrides, disabledDrives);\n const totalPressure = drives.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = drives\n .filter(d => d.pressure > 0)\n .sort((a, b) => b.pressure - a.pressure)\n .slice(0, 2)\n .map(d => d.id);\n return {\n timestamp: new Date().toISOString(),\n drives,\n totalPressure,\n dominantDrives,\n };\n}\n"],"mappings":"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","names":[]}
@@ -168,6 +168,22 @@ async function runPressureCycle(drives) {
168
168
  for (const d of reading.perDrive.slice(0, 3)) {
169
169
  noteLines.push(`- ${d.id}: pressure ${d.pressure.toFixed(2)} \u2014 ${d.description}`);
170
170
  }
171
+ if (decision.dominantDrives[0] === "social") {
172
+ const socialDrive = drives.find((d) => d.id === "social");
173
+ const hoursSince = socialDrive?.inputs?.hoursSinceLastPost ?? 0;
174
+ if (hoursSince >= 6) {
175
+ noteLines.push(
176
+ "",
177
+ "PROPOSAL HINT: Social drive deficit is driven by Facebook posting drought.",
178
+ `It has been ~${Math.round(hoursSince)}h since the last FB post.`,
179
+ "Propose a goal of type `facebook_post` with one of these contentTypes:",
180
+ ' - "activity" \u2014 post real TITAN runtime activity from the last 24h',
181
+ ' - "stats" \u2014 post download/install milestones if any crossed today',
182
+ ' - "promo" \u2014 promo a recent feature shipment (only if a release tagged today)',
183
+ "Only propose if there is genuine activity to share \u2014 empty/generic posts are worse than none."
184
+ );
185
+ }
186
+ }
171
187
  const consolidationNotes = noteLines.join("\n");
172
188
  const somaAgentId = `soma:${decision.dominantDrives[0] ?? "fused"}`;
173
189
  let approvalId;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/organism/pressure.ts"],"sourcesContent":["/**\n * TITAN — Pressure Fusion (Soma)\n *\n * Turns drive deficits into proposals. When combined pressure across drives\n * crosses `config.organism.pressureThreshold`, Soma builds a GoalProposal\n * seeded with drive-specific context and routes it through the existing\n * goalProposer / commandPost approval plumbing (F1 landed the pipe;\n * pressure fusion is the new trigger source).\n *\n * Key invariants:\n * - This is the ONLY path that converts pressure → proposals. No ad-hoc\n * proposal generation elsewhere in the organism layer.\n * - All proposals run through rehearseShadow() before approval is filed.\n * - The existing F1 rate limit (config.agent.proposalRateLimitPerDay)\n * applies — Soma can't spam proposals faster than the agent-level cap.\n */\nimport type { DriveState, DriveId } from './drives.js';\nimport { rehearseShadow, type ShadowVerdict } from './shadow.js';\nimport { emit } from '../substrate/traceBus.js';\nimport { loadConfig } from '../config/config.js';\nimport { readJsonFile, writeJsonFile } from '../utils/helpers.js';\nimport { SOMADRIVE_STATE_PATH } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Pressure';\n\n/**\n * v4.6.0: per-drive fire history. Used to damp consecutive proposals for\n * the same drive so we don't spawn duplicate goals every tick.\n * Persisted to disk so damping survives restarts.\n */\nconst lastFireByDrive = new Map<string, number>();\nlet lastGlobalFire = 0;\n\n// Load persisted damping state on module init.\n(function loadDampingState() {\n const raw = readJsonFile<Record<string, number>>(SOMADRIVE_STATE_PATH);\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n const now = Date.now();\n const DAMPING_MS = 2 * 60 * 60 * 1000; // v5.0.0: increased to 2h\n for (const [k, v] of Object.entries(raw)) {\n if (typeof v === 'number' && now - v < DAMPING_MS * 2) {\n lastFireByDrive.set(k, v);\n }\n }\n }\n})();\n\nfunction saveDampingState() {\n const obj: Record<string, number> = {};\n for (const [k, v] of lastFireByDrive) obj[k] = v;\n writeJsonFile(SOMADRIVE_STATE_PATH, obj);\n}\n\n/**\n * Test-only hook: clear the per-drive damping memory so unit tests that\n * exercise consecutive `runPressureCycle` calls on the same drive don't\n * leak state across `beforeEach` boundaries. Not part of the public API\n * for runtime callers — production never needs to reset this.\n */\nexport function _resetPressureDampingForTests(): void {\n lastFireByDrive.clear();\n lastGlobalFire = 0;\n}\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface PressureReading {\n totalPressure: number;\n dominantDrives: DriveId[];\n perDrive: Array<{ id: DriveId; pressure: number; description: string }>;\n}\n\nexport interface PressureDecision {\n should: boolean;\n totalPressure: number;\n threshold: number;\n dominantDrives: DriveId[];\n reason: string;\n}\n\n// ── Pressure accounting ──────────────────────────────────────────\n\nexport function computePressureReading(drives: DriveState[]): PressureReading {\n const perDrive = drives\n .filter(d => d.pressure > 0)\n .map(d => ({ id: d.id as DriveId, pressure: d.pressure, description: d.description }))\n .sort((a, b) => b.pressure - a.pressure);\n const totalPressure = perDrive.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = perDrive.slice(0, 2).map(d => d.id);\n return { totalPressure, dominantDrives, perDrive };\n}\n\n/** Deterministic threshold check. Does NOT fire any side effects. */\nexport function evaluatePressure(drives: DriveState[], threshold: number): PressureDecision {\n const reading = computePressureReading(drives);\n if (reading.totalPressure < threshold) {\n return {\n should: false,\n totalPressure: reading.totalPressure,\n threshold,\n dominantDrives: reading.dominantDrives,\n reason: `total pressure ${reading.totalPressure.toFixed(2)} below threshold ${threshold}`,\n };\n }\n const topPhrases = reading.perDrive.slice(0, 2).map(d => `${d.id} (${d.pressure.toFixed(2)})`);\n return {\n should: true,\n totalPressure: reading.totalPressure,\n threshold,\n dominantDrives: reading.dominantDrives,\n reason: `dominant drives: ${topPhrases.join(', ')}`,\n };\n}\n\n// ── Proposal driver ──────────────────────────────────────────────\n\nexport interface PressureCycleResult {\n fired: boolean;\n reading: PressureReading;\n decision: PressureDecision;\n approvalId?: string;\n shadow?: ShadowVerdict;\n skipped?: string;\n}\n\n/**\n * One pressure cycle: evaluate → maybe build context → rehearse → file\n * approval. Uses the F1 `requestGoalProposalApproval` / goalProposer\n * pipeline — does NOT create a parallel approval path.\n *\n * Returns a structured result so the UI / activity feed can record exactly\n * what happened on this cycle even when nothing fires.\n */\nexport async function runPressureCycle(\n drives: DriveState[],\n): Promise<PressureCycleResult> {\n const config = loadConfig();\n const organism = (config as unknown as {\n organism?: {\n enabled?: boolean;\n pressureThreshold?: number;\n shadowEnabled?: boolean;\n shadowModel?: string;\n };\n }).organism || {};\n if (!organism.enabled) {\n const reading = computePressureReading(drives);\n return {\n fired: false,\n reading,\n decision: {\n should: false, totalPressure: reading.totalPressure, threshold: 0,\n dominantDrives: reading.dominantDrives, reason: 'organism disabled',\n },\n skipped: 'organism.enabled=false',\n };\n }\n\n // v4.9.0: if the kill switch fired, refuse to run the pressure cycle.\n // Goals + specialists already got paused by the kill sequence; we\n // must not propose more work until Tony resumes.\n try {\n const { isKilled } = await import('../safety/killSwitch.js');\n if (isKilled()) {\n const reading = computePressureReading(drives);\n return {\n fired: false,\n reading,\n decision: {\n should: false, totalPressure: reading.totalPressure, threshold: 0,\n dominantDrives: reading.dominantDrives, reason: 'kill switch active',\n },\n skipped: 'kill switch active — awaiting human resume',\n };\n }\n } catch { /* safety module unavailable — continue */ }\n\n // v4.9.0: evaluate sustained Safety pressure so the kill switch\n // can fire if Safety stays > 2.0 for 10 minutes. Best-effort.\n try {\n const safety = drives.find(d => d.id === 'safety');\n if (safety) {\n const { evaluateSafetyPressure } = await import('../safety/killSwitch.js');\n evaluateSafetyPressure(safety.pressure);\n }\n } catch { /* ok */ }\n const threshold = organism.pressureThreshold ?? 1.2;\n const decision = evaluatePressure(drives, threshold);\n const reading = computePressureReading(drives);\n\n if (!decision.should) {\n return { fired: false, reading, decision };\n }\n\n // v5.0.0: Global cooldown + per-drive backoff + goal-overload detection.\n const now = Date.now();\n const GLOBAL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour max across ALL drives\n if (now - lastGlobalFire < GLOBAL_COOLDOWN_MS) {\n return {\n fired: false, reading, decision,\n skipped: `global cooldown: last SOMA proposal ${Math.round((now - lastGlobalFire) / 60_000)}m ago (min 60m)`,\n };\n }\n\n const dominantId = decision.dominantDrives[0];\n if (dominantId) {\n const last = lastFireByDrive.get(dominantId) || 0;\n const DAMPING_MS = 2 * 60 * 60 * 1000; // v5.0.0: 2h per drive\n if (now - last < DAMPING_MS) {\n return {\n fired: false, reading, decision,\n skipped: `drive ${dominantId} fired ${Math.round((now - last) / 60_000)}m ago — damping until 2h elapsed`,\n };\n }\n // Check if the drive already has active goals in flight. If N ≥ 2,\n // give existing work more time before stacking on more.\n try {\n const { listGoals } = await import('../agent/goals.js');\n const allGoals = listGoals();\n const activeCount = allGoals.filter(g => g.status === 'active').length;\n\n // Goal overload: if there are too many active goals, refuse to add MORE.\n // Instead the organism should focus on completing existing work.\n if (activeCount >= 30) {\n return {\n fired: false, reading, decision,\n skipped: `goal overload: ${activeCount} active goals — organism focuses on existing work before proposing more`,\n };\n }\n\n const activeForDrive = allGoals.filter(g => {\n if (g.status !== 'active') return false;\n const tags = g.tags || [];\n const text = `${g.title} ${g.description || ''}`.toLowerCase();\n return tags.includes(`soma:${dominantId}`) || text.includes(dominantId);\n }).length;\n if (activeForDrive >= 2) {\n return {\n fired: false, reading, decision,\n skipped: `drive ${dominantId} already has ${activeForDrive} active goals — letting existing work complete`,\n };\n }\n } catch { /* best-effort */ }\n }\n\n emit('pressure:threshold', {\n timestamp: new Date().toISOString(),\n totalPressure: decision.totalPressure,\n threshold,\n dominantDrives: decision.dominantDrives,\n reason: decision.reason,\n });\n\n // Build a drive-specific context note the proposer will use to seed its\n // suggestion. The proposer (F1) already accepts `consolidationNotes`.\n const noteLines = ['Autonomous pressure crossed threshold.'];\n for (const d of reading.perDrive.slice(0, 3)) {\n noteLines.push(`- ${d.id}: pressure ${d.pressure.toFixed(2)} — ${d.description}`);\n }\n const consolidationNotes = noteLines.join('\\n');\n\n // Pressure-driven proposer uses the agent id `soma:${dominantDrive}` so\n // the activity feed attributes the proposal to the organism, not to a\n // registered agent. This also keeps the F1 rate limit per-\"agent\" —\n // each dominant drive has its own per-day budget.\n const somaAgentId = `soma:${decision.dominantDrives[0] ?? 'fused'}`;\n\n // Dynamic import to avoid a module cycle with commandPost:\n // pressure -> commandPost (createApproval) and\n // commandPost listeners <- drives <- pressure are both reachable.\n let approvalId: string | undefined;\n let shadow: ShadowVerdict | undefined;\n try {\n const { generateGoalProposals } = await import('../agent/goalProposer.js');\n const { requestGoalProposalApproval, getApproval, attachShadowVerdictToApproval } =\n await import('../agent/commandPost.js');\n\n // generateGoalProposals uses loadConfig().agent.autoProposeGoals — for\n // Soma-driven flow we want the proposer to run whether or not\n // autoProposeGoals is globally on. Direct-call approach: build a\n // context with our notes and invoke the proposer with a bypass flag.\n const approvals = await generateGoalProposals(somaAgentId, {\n activeGoals: drives.map(d => `${d.label} at ${Math.round(d.satisfaction * 100)}%`),\n consolidationNotes,\n }, 'soma_proposal');\n\n if (approvals.length === 0) {\n return {\n fired: false,\n reading,\n decision,\n skipped: 'proposer returned no actionable proposals (below quality bar or rate-limited)',\n };\n }\n\n // v4.0.5: shadow-rehearse EVERY proposal returned by the proposer,\n // not just approvals[0]. Earlier cycles that returned 2+ proposals\n // left the extras without a shadow verdict on the approval payload.\n // The first approval is still the \"primary\" returned in the result\n // for backward compat with callers expecting a single approvalId.\n const primary = approvals[0];\n approvalId = primary.id;\n\n if (organism.shadowEnabled !== false) {\n for (const approval of approvals) {\n try {\n const verdict = await rehearseShadow({\n title: (approval.payload as { title?: string })?.title ?? '(unspecified)',\n description: (approval.payload as { description?: string })?.description ?? '',\n rationale: (approval.payload as { rationale?: string })?.rationale ?? '',\n }, organism.shadowModel);\n attachShadowVerdictToApproval(approval.id, verdict as unknown as Record<string, unknown>);\n if (approval.id === primary.id) shadow = verdict;\n } catch (err) {\n logger.warn(COMPONENT, `Shadow rehearsal failed for ${approval.id}: ${(err as Error).message}`);\n }\n }\n }\n\n // Emit one soma:proposal per approval so UI + activity feed see each.\n for (const approval of approvals) {\n const currentVerdict = (approval.payload as { shadowVerdict?: ShadowVerdict })?.shadowVerdict;\n emit('soma:proposal', {\n timestamp: new Date().toISOString(),\n approvalId: approval.id,\n proposedBy: somaAgentId,\n title: (approval.payload as { title?: string })?.title ?? '',\n description: (approval.payload as { description?: string })?.description ?? '',\n rationale: (approval.payload as { rationale?: string })?.rationale ?? '',\n dominantDrives: decision.dominantDrives,\n shadowVerdict: currentVerdict ? {\n reversibilityScore: currentVerdict.reversibilityScore,\n estimatedCostUsd: currentVerdict.estimatedCostUsd,\n breakRisks: currentVerdict.breakRisks,\n } : undefined,\n });\n }\n\n // Quiet the 'unused' check on getApproval — we may use it for logging.\n void getApproval;\n void requestGoalProposalApproval;\n\n // v5.0.0: record fire timestamps for damping on next tick.\n lastGlobalFire = Date.now();\n if (dominantId) {\n lastFireByDrive.set(dominantId, Date.now());\n saveDampingState();\n }\n\n logger.info(COMPONENT, `Soma fired ${approvals.length} proposal(s), primary=${primary.id}: ${decision.reason}`);\n return { fired: true, reading, decision, approvalId, shadow };\n } catch (err) {\n logger.warn(COMPONENT, `Pressure cycle failed: ${(err as Error).message}`);\n return { fired: false, reading, decision, skipped: `error: ${(err as Error).message}` };\n }\n}\n"],"mappings":";AAiBA,SAAS,sBAA0C;AACnD,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,cAAc,qBAAqB;AAC5C,SAAS,4BAA4B;AACrC,OAAO,YAAY;AAEnB,MAAM,YAAY;AAOlB,MAAM,kBAAkB,oBAAI,IAAoB;AAChD,IAAI,iBAAiB;AAAA,CAGpB,SAAS,mBAAmB;AACzB,QAAM,MAAM,aAAqC,oBAAoB;AACrE,MAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AACvD,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,aAAa,IAAI,KAAK,KAAK;AACjC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACtC,UAAI,OAAO,MAAM,YAAY,MAAM,IAAI,aAAa,GAAG;AACnD,wBAAgB,IAAI,GAAG,CAAC;AAAA,MAC5B;AAAA,IACJ;AAAA,EACJ;AACJ,GAAG;AAEH,SAAS,mBAAmB;AACxB,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,gBAAiB,KAAI,CAAC,IAAI;AAC/C,gBAAc,sBAAsB,GAAG;AAC3C;AAQO,SAAS,gCAAsC;AAClD,kBAAgB,MAAM;AACtB,mBAAiB;AACrB;AAoBO,SAAS,uBAAuB,QAAuC;AAC1E,QAAM,WAAW,OACZ,OAAO,OAAK,EAAE,WAAW,CAAC,EAC1B,IAAI,QAAM,EAAE,IAAI,EAAE,IAAe,UAAU,EAAE,UAAU,aAAa,EAAE,YAAY,EAAE,EACpF,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAC3C,QAAM,gBAAgB,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,UAAU,CAAC;AACrE,QAAM,iBAAiB,SAAS,MAAM,GAAG,CAAC,EAAE,IAAI,OAAK,EAAE,EAAE;AACzD,SAAO,EAAE,eAAe,gBAAgB,SAAS;AACrD;AAGO,SAAS,iBAAiB,QAAsB,WAAqC;AACxF,QAAM,UAAU,uBAAuB,MAAM;AAC7C,MAAI,QAAQ,gBAAgB,WAAW;AACnC,WAAO;AAAA,MACH,QAAQ;AAAA,MACR,eAAe,QAAQ;AAAA,MACvB;AAAA,MACA,gBAAgB,QAAQ;AAAA,MACxB,QAAQ,kBAAkB,QAAQ,cAAc,QAAQ,CAAC,CAAC,oBAAoB,SAAS;AAAA,IAC3F;AAAA,EACJ;AACA,QAAM,aAAa,QAAQ,SAAS,MAAM,GAAG,CAAC,EAAE,IAAI,OAAK,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,QAAQ,CAAC,CAAC,GAAG;AAC7F,SAAO;AAAA,IACH,QAAQ;AAAA,IACR,eAAe,QAAQ;AAAA,IACvB;AAAA,IACA,gBAAgB,QAAQ;AAAA,IACxB,QAAQ,oBAAoB,WAAW,KAAK,IAAI,CAAC;AAAA,EACrD;AACJ;AAqBA,eAAsB,iBAClB,QAC4B;AAC5B,QAAM,SAAS,WAAW;AAC1B,QAAM,WAAY,OAOf,YAAY,CAAC;AAChB,MAAI,CAAC,SAAS,SAAS;AACnB,UAAMA,WAAU,uBAAuB,MAAM;AAC7C,WAAO;AAAA,MACH,OAAO;AAAA,MACP,SAAAA;AAAA,MACA,UAAU;AAAA,QACN,QAAQ;AAAA,QAAO,eAAeA,SAAQ;AAAA,QAAe,WAAW;AAAA,QAChE,gBAAgBA,SAAQ;AAAA,QAAgB,QAAQ;AAAA,MACpD;AAAA,MACA,SAAS;AAAA,IACb;AAAA,EACJ;AAKA,MAAI;AACA,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,yBAAyB;AAC3D,QAAI,SAAS,GAAG;AACZ,YAAMA,WAAU,uBAAuB,MAAM;AAC7C,aAAO;AAAA,QACH,OAAO;AAAA,QACP,SAAAA;AAAA,QACA,UAAU;AAAA,UACN,QAAQ;AAAA,UAAO,eAAeA,SAAQ;AAAA,UAAe,WAAW;AAAA,UAChE,gBAAgBA,SAAQ;AAAA,UAAgB,QAAQ;AAAA,QACpD;AAAA,QACA,SAAS;AAAA,MACb;AAAA,IACJ;AAAA,EACJ,QAAQ;AAAA,EAA6C;AAIrD,MAAI;AACA,UAAM,SAAS,OAAO,KAAK,OAAK,EAAE,OAAO,QAAQ;AACjD,QAAI,QAAQ;AACR,YAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,yBAAyB;AACzE,6BAAuB,OAAO,QAAQ;AAAA,IAC1C;AAAA,EACJ,QAAQ;AAAA,EAAW;AACnB,QAAM,YAAY,SAAS,qBAAqB;AAChD,QAAM,WAAW,iBAAiB,QAAQ,SAAS;AACnD,QAAM,UAAU,uBAAuB,MAAM;AAE7C,MAAI,CAAC,SAAS,QAAQ;AAClB,WAAO,EAAE,OAAO,OAAO,SAAS,SAAS;AAAA,EAC7C;AAGA,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,qBAAqB,KAAK,KAAK;AACrC,MAAI,MAAM,iBAAiB,oBAAoB;AAC3C,WAAO;AAAA,MACH,OAAO;AAAA,MAAO;AAAA,MAAS;AAAA,MACvB,SAAS,uCAAuC,KAAK,OAAO,MAAM,kBAAkB,GAAM,CAAC;AAAA,IAC/F;AAAA,EACJ;AAEA,QAAM,aAAa,SAAS,eAAe,CAAC;AAC5C,MAAI,YAAY;AACZ,UAAM,OAAO,gBAAgB,IAAI,UAAU,KAAK;AAChD,UAAM,aAAa,IAAI,KAAK,KAAK;AACjC,QAAI,MAAM,OAAO,YAAY;AACzB,aAAO;AAAA,QACH,OAAO;AAAA,QAAO;AAAA,QAAS;AAAA,QACvB,SAAS,SAAS,UAAU,UAAU,KAAK,OAAO,MAAM,QAAQ,GAAM,CAAC;AAAA,MAC3E;AAAA,IACJ;AAGA,QAAI;AACA,YAAM,EAAE,UAAU,IAAI,MAAM,OAAO,mBAAmB;AACtD,YAAM,WAAW,UAAU;AAC3B,YAAM,cAAc,SAAS,OAAO,OAAK,EAAE,WAAW,QAAQ,EAAE;AAIhE,UAAI,eAAe,IAAI;AACnB,eAAO;AAAA,UACH,OAAO;AAAA,UAAO;AAAA,UAAS;AAAA,UACvB,SAAS,kBAAkB,WAAW;AAAA,QAC1C;AAAA,MACJ;AAEA,YAAM,iBAAiB,SAAS,OAAO,OAAK;AACxC,YAAI,EAAE,WAAW,SAAU,QAAO;AAClC,cAAM,OAAO,EAAE,QAAQ,CAAC;AACxB,cAAM,OAAO,GAAG,EAAE,KAAK,IAAI,EAAE,eAAe,EAAE,GAAG,YAAY;AAC7D,eAAO,KAAK,SAAS,QAAQ,UAAU,EAAE,KAAK,KAAK,SAAS,UAAU;AAAA,MAC1E,CAAC,EAAE;AACH,UAAI,kBAAkB,GAAG;AACrB,eAAO;AAAA,UACH,OAAO;AAAA,UAAO;AAAA,UAAS;AAAA,UACvB,SAAS,SAAS,UAAU,gBAAgB,cAAc;AAAA,QAC9D;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAAoB;AAAA,EAChC;AAEA,OAAK,sBAAsB;AAAA,IACvB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,eAAe,SAAS;AAAA,IACxB;AAAA,IACA,gBAAgB,SAAS;AAAA,IACzB,QAAQ,SAAS;AAAA,EACrB,CAAC;AAID,QAAM,YAAY,CAAC,wCAAwC;AAC3D,aAAW,KAAK,QAAQ,SAAS,MAAM,GAAG,CAAC,GAAG;AAC1C,cAAU,KAAK,KAAK,EAAE,EAAE,cAAc,EAAE,SAAS,QAAQ,CAAC,CAAC,WAAM,EAAE,WAAW,EAAE;AAAA,EACpF;AACA,QAAM,qBAAqB,UAAU,KAAK,IAAI;AAM9C,QAAM,cAAc,QAAQ,SAAS,eAAe,CAAC,KAAK,OAAO;AAKjE,MAAI;AACJ,MAAI;AACJ,MAAI;AACA,UAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,0BAA0B;AACzE,UAAM,EAAE,6BAA6B,aAAa,8BAA8B,IAC5E,MAAM,OAAO,yBAAyB;AAM1C,UAAM,YAAY,MAAM,sBAAsB,aAAa;AAAA,MACvD,aAAa,OAAO,IAAI,OAAK,GAAG,EAAE,KAAK,OAAO,KAAK,MAAM,EAAE,eAAe,GAAG,CAAC,GAAG;AAAA,MACjF;AAAA,IACJ,GAAG,eAAe;AAElB,QAAI,UAAU,WAAW,GAAG;AACxB,aAAO;AAAA,QACH,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,SAAS;AAAA,MACb;AAAA,IACJ;AAOA,UAAM,UAAU,UAAU,CAAC;AAC3B,iBAAa,QAAQ;AAErB,QAAI,SAAS,kBAAkB,OAAO;AAClC,iBAAW,YAAY,WAAW;AAC9B,YAAI;AACA,gBAAM,UAAU,MAAM,eAAe;AAAA,YACjC,OAAQ,SAAS,SAAgC,SAAS;AAAA,YAC1D,aAAc,SAAS,SAAsC,eAAe;AAAA,YAC5E,WAAY,SAAS,SAAoC,aAAa;AAAA,UAC1E,GAAG,SAAS,WAAW;AACvB,wCAA8B,SAAS,IAAI,OAA6C;AACxF,cAAI,SAAS,OAAO,QAAQ,GAAI,UAAS;AAAA,QAC7C,SAAS,KAAK;AACV,iBAAO,KAAK,WAAW,+BAA+B,SAAS,EAAE,KAAM,IAAc,OAAO,EAAE;AAAA,QAClG;AAAA,MACJ;AAAA,IACJ;AAGA,eAAW,YAAY,WAAW;AAC9B,YAAM,iBAAkB,SAAS,SAA+C;AAChF,WAAK,iBAAiB;AAAA,QAClB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,YAAY,SAAS;AAAA,QACrB,YAAY;AAAA,QACZ,OAAQ,SAAS,SAAgC,SAAS;AAAA,QAC1D,aAAc,SAAS,SAAsC,eAAe;AAAA,QAC5E,WAAY,SAAS,SAAoC,aAAa;AAAA,QACtE,gBAAgB,SAAS;AAAA,QACzB,eAAe,iBAAiB;AAAA,UAC5B,oBAAoB,eAAe;AAAA,UACnC,kBAAkB,eAAe;AAAA,UACjC,YAAY,eAAe;AAAA,QAC/B,IAAI;AAAA,MACR,CAAC;AAAA,IACL;AAGA,SAAK;AACL,SAAK;AAGL,qBAAiB,KAAK,IAAI;AAC1B,QAAI,YAAY;AACZ,sBAAgB,IAAI,YAAY,KAAK,IAAI,CAAC;AAC1C,uBAAiB;AAAA,IACrB;AAEA,WAAO,KAAK,WAAW,cAAc,UAAU,MAAM,yBAAyB,QAAQ,EAAE,KAAK,SAAS,MAAM,EAAE;AAC9G,WAAO,EAAE,OAAO,MAAM,SAAS,UAAU,YAAY,OAAO;AAAA,EAChE,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,0BAA2B,IAAc,OAAO,EAAE;AACzE,WAAO,EAAE,OAAO,OAAO,SAAS,UAAU,SAAS,UAAW,IAAc,OAAO,GAAG;AAAA,EAC1F;AACJ;","names":["reading"]}
1
+ {"version":3,"sources":["../../src/organism/pressure.ts"],"sourcesContent":["/**\n * TITAN — Pressure Fusion (Soma)\n *\n * Turns drive deficits into proposals. When combined pressure across drives\n * crosses `config.organism.pressureThreshold`, Soma builds a GoalProposal\n * seeded with drive-specific context and routes it through the existing\n * goalProposer / commandPost approval plumbing (F1 landed the pipe;\n * pressure fusion is the new trigger source).\n *\n * Key invariants:\n * - This is the ONLY path that converts pressure → proposals. No ad-hoc\n * proposal generation elsewhere in the organism layer.\n * - All proposals run through rehearseShadow() before approval is filed.\n * - The existing F1 rate limit (config.agent.proposalRateLimitPerDay)\n * applies — Soma can't spam proposals faster than the agent-level cap.\n */\nimport type { DriveState, DriveId } from './drives.js';\nimport { rehearseShadow, type ShadowVerdict } from './shadow.js';\nimport { emit } from '../substrate/traceBus.js';\nimport { loadConfig } from '../config/config.js';\nimport { readJsonFile, writeJsonFile } from '../utils/helpers.js';\nimport { SOMADRIVE_STATE_PATH } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Pressure';\n\n/**\n * v4.6.0: per-drive fire history. Used to damp consecutive proposals for\n * the same drive so we don't spawn duplicate goals every tick.\n * Persisted to disk so damping survives restarts.\n */\nconst lastFireByDrive = new Map<string, number>();\nlet lastGlobalFire = 0;\n\n// Load persisted damping state on module init.\n(function loadDampingState() {\n const raw = readJsonFile<Record<string, number>>(SOMADRIVE_STATE_PATH);\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n const now = Date.now();\n const DAMPING_MS = 2 * 60 * 60 * 1000; // v5.0.0: increased to 2h\n for (const [k, v] of Object.entries(raw)) {\n if (typeof v === 'number' && now - v < DAMPING_MS * 2) {\n lastFireByDrive.set(k, v);\n }\n }\n }\n})();\n\nfunction saveDampingState() {\n const obj: Record<string, number> = {};\n for (const [k, v] of lastFireByDrive) obj[k] = v;\n writeJsonFile(SOMADRIVE_STATE_PATH, obj);\n}\n\n/**\n * Test-only hook: clear the per-drive damping memory so unit tests that\n * exercise consecutive `runPressureCycle` calls on the same drive don't\n * leak state across `beforeEach` boundaries. Not part of the public API\n * for runtime callers — production never needs to reset this.\n */\nexport function _resetPressureDampingForTests(): void {\n lastFireByDrive.clear();\n lastGlobalFire = 0;\n}\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface PressureReading {\n totalPressure: number;\n dominantDrives: DriveId[];\n perDrive: Array<{ id: DriveId; pressure: number; description: string }>;\n}\n\nexport interface PressureDecision {\n should: boolean;\n totalPressure: number;\n threshold: number;\n dominantDrives: DriveId[];\n reason: string;\n}\n\n// ── Pressure accounting ──────────────────────────────────────────\n\nexport function computePressureReading(drives: DriveState[]): PressureReading {\n const perDrive = drives\n .filter(d => d.pressure > 0)\n .map(d => ({ id: d.id as DriveId, pressure: d.pressure, description: d.description }))\n .sort((a, b) => b.pressure - a.pressure);\n const totalPressure = perDrive.reduce((sum, d) => sum + d.pressure, 0);\n const dominantDrives = perDrive.slice(0, 2).map(d => d.id);\n return { totalPressure, dominantDrives, perDrive };\n}\n\n/** Deterministic threshold check. Does NOT fire any side effects. */\nexport function evaluatePressure(drives: DriveState[], threshold: number): PressureDecision {\n const reading = computePressureReading(drives);\n if (reading.totalPressure < threshold) {\n return {\n should: false,\n totalPressure: reading.totalPressure,\n threshold,\n dominantDrives: reading.dominantDrives,\n reason: `total pressure ${reading.totalPressure.toFixed(2)} below threshold ${threshold}`,\n };\n }\n const topPhrases = reading.perDrive.slice(0, 2).map(d => `${d.id} (${d.pressure.toFixed(2)})`);\n return {\n should: true,\n totalPressure: reading.totalPressure,\n threshold,\n dominantDrives: reading.dominantDrives,\n reason: `dominant drives: ${topPhrases.join(', ')}`,\n };\n}\n\n// ── Proposal driver ──────────────────────────────────────────────\n\nexport interface PressureCycleResult {\n fired: boolean;\n reading: PressureReading;\n decision: PressureDecision;\n approvalId?: string;\n shadow?: ShadowVerdict;\n skipped?: string;\n}\n\n/**\n * One pressure cycle: evaluate → maybe build context → rehearse → file\n * approval. Uses the F1 `requestGoalProposalApproval` / goalProposer\n * pipeline — does NOT create a parallel approval path.\n *\n * Returns a structured result so the UI / activity feed can record exactly\n * what happened on this cycle even when nothing fires.\n */\nexport async function runPressureCycle(\n drives: DriveState[],\n): Promise<PressureCycleResult> {\n const config = loadConfig();\n const organism = (config as unknown as {\n organism?: {\n enabled?: boolean;\n pressureThreshold?: number;\n shadowEnabled?: boolean;\n shadowModel?: string;\n };\n }).organism || {};\n if (!organism.enabled) {\n const reading = computePressureReading(drives);\n return {\n fired: false,\n reading,\n decision: {\n should: false, totalPressure: reading.totalPressure, threshold: 0,\n dominantDrives: reading.dominantDrives, reason: 'organism disabled',\n },\n skipped: 'organism.enabled=false',\n };\n }\n\n // v4.9.0: if the kill switch fired, refuse to run the pressure cycle.\n // Goals + specialists already got paused by the kill sequence; we\n // must not propose more work until Tony resumes.\n try {\n const { isKilled } = await import('../safety/killSwitch.js');\n if (isKilled()) {\n const reading = computePressureReading(drives);\n return {\n fired: false,\n reading,\n decision: {\n should: false, totalPressure: reading.totalPressure, threshold: 0,\n dominantDrives: reading.dominantDrives, reason: 'kill switch active',\n },\n skipped: 'kill switch active — awaiting human resume',\n };\n }\n } catch { /* safety module unavailable — continue */ }\n\n // v4.9.0: evaluate sustained Safety pressure so the kill switch\n // can fire if Safety stays > 2.0 for 10 minutes. Best-effort.\n try {\n const safety = drives.find(d => d.id === 'safety');\n if (safety) {\n const { evaluateSafetyPressure } = await import('../safety/killSwitch.js');\n evaluateSafetyPressure(safety.pressure);\n }\n } catch { /* ok */ }\n const threshold = organism.pressureThreshold ?? 1.2;\n const decision = evaluatePressure(drives, threshold);\n const reading = computePressureReading(drives);\n\n if (!decision.should) {\n return { fired: false, reading, decision };\n }\n\n // v5.0.0: Global cooldown + per-drive backoff + goal-overload detection.\n const now = Date.now();\n const GLOBAL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour max across ALL drives\n if (now - lastGlobalFire < GLOBAL_COOLDOWN_MS) {\n return {\n fired: false, reading, decision,\n skipped: `global cooldown: last SOMA proposal ${Math.round((now - lastGlobalFire) / 60_000)}m ago (min 60m)`,\n };\n }\n\n const dominantId = decision.dominantDrives[0];\n if (dominantId) {\n const last = lastFireByDrive.get(dominantId) || 0;\n const DAMPING_MS = 2 * 60 * 60 * 1000; // v5.0.0: 2h per drive\n if (now - last < DAMPING_MS) {\n return {\n fired: false, reading, decision,\n skipped: `drive ${dominantId} fired ${Math.round((now - last) / 60_000)}m ago — damping until 2h elapsed`,\n };\n }\n // Check if the drive already has active goals in flight. If N ≥ 2,\n // give existing work more time before stacking on more.\n try {\n const { listGoals } = await import('../agent/goals.js');\n const allGoals = listGoals();\n const activeCount = allGoals.filter(g => g.status === 'active').length;\n\n // Goal overload: if there are too many active goals, refuse to add MORE.\n // Instead the organism should focus on completing existing work.\n if (activeCount >= 30) {\n return {\n fired: false, reading, decision,\n skipped: `goal overload: ${activeCount} active goals — organism focuses on existing work before proposing more`,\n };\n }\n\n const activeForDrive = allGoals.filter(g => {\n if (g.status !== 'active') return false;\n const tags = g.tags || [];\n const text = `${g.title} ${g.description || ''}`.toLowerCase();\n return tags.includes(`soma:${dominantId}`) || text.includes(dominantId);\n }).length;\n if (activeForDrive >= 2) {\n return {\n fired: false, reading, decision,\n skipped: `drive ${dominantId} already has ${activeForDrive} active goals — letting existing work complete`,\n };\n }\n } catch { /* best-effort */ }\n }\n\n emit('pressure:threshold', {\n timestamp: new Date().toISOString(),\n totalPressure: decision.totalPressure,\n threshold,\n dominantDrives: decision.dominantDrives,\n reason: decision.reason,\n });\n\n // Build a drive-specific context note the proposer will use to seed its\n // suggestion. The proposer (F1) already accepts `consolidationNotes`.\n const noteLines = ['Autonomous pressure crossed threshold.'];\n for (const d of reading.perDrive.slice(0, 3)) {\n noteLines.push(`- ${d.id}: pressure ${d.pressure.toFixed(2)} — ${d.description}`);\n }\n\n // v5.3.2 Track B: when Social drive is dominant, point the proposer at\n // a concrete `facebook_post` action. Without this hint the proposer\n // sees \"social pressure high\" and might propose anything (run a sub-\n // agent, generate a status report) — none of which satisfies the\n // actual deficit. The Social drive now blends agent staleness +\n // time-since-last-FB-post; if the dominant cause is the posting\n // drought, propose a post.\n if (decision.dominantDrives[0] === 'social') {\n // perDrive doesn't carry inputs — read them off DriveState directly.\n const socialDrive = drives.find(d => d.id === 'social');\n const hoursSince = (socialDrive?.inputs?.hoursSinceLastPost as number) ?? 0;\n if (hoursSince >= 6) {\n noteLines.push(\n '',\n 'PROPOSAL HINT: Social drive deficit is driven by Facebook posting drought.',\n `It has been ~${Math.round(hoursSince)}h since the last FB post.`,\n 'Propose a goal of type `facebook_post` with one of these contentTypes:',\n ' - \"activity\" — post real TITAN runtime activity from the last 24h',\n ' - \"stats\" — post download/install milestones if any crossed today',\n ' - \"promo\" — promo a recent feature shipment (only if a release tagged today)',\n 'Only propose if there is genuine activity to share — empty/generic posts are worse than none.',\n );\n }\n }\n\n const consolidationNotes = noteLines.join('\\n');\n\n // Pressure-driven proposer uses the agent id `soma:${dominantDrive}` so\n // the activity feed attributes the proposal to the organism, not to a\n // registered agent. This also keeps the F1 rate limit per-\"agent\" —\n // each dominant drive has its own per-day budget.\n const somaAgentId = `soma:${decision.dominantDrives[0] ?? 'fused'}`;\n\n // Dynamic import to avoid a module cycle with commandPost:\n // pressure -> commandPost (createApproval) and\n // commandPost listeners <- drives <- pressure are both reachable.\n let approvalId: string | undefined;\n let shadow: ShadowVerdict | undefined;\n try {\n const { generateGoalProposals } = await import('../agent/goalProposer.js');\n const { requestGoalProposalApproval, getApproval, attachShadowVerdictToApproval } =\n await import('../agent/commandPost.js');\n\n // generateGoalProposals uses loadConfig().agent.autoProposeGoals — for\n // Soma-driven flow we want the proposer to run whether or not\n // autoProposeGoals is globally on. Direct-call approach: build a\n // context with our notes and invoke the proposer with a bypass flag.\n const approvals = await generateGoalProposals(somaAgentId, {\n activeGoals: drives.map(d => `${d.label} at ${Math.round(d.satisfaction * 100)}%`),\n consolidationNotes,\n }, 'soma_proposal');\n\n if (approvals.length === 0) {\n return {\n fired: false,\n reading,\n decision,\n skipped: 'proposer returned no actionable proposals (below quality bar or rate-limited)',\n };\n }\n\n // v4.0.5: shadow-rehearse EVERY proposal returned by the proposer,\n // not just approvals[0]. Earlier cycles that returned 2+ proposals\n // left the extras without a shadow verdict on the approval payload.\n // The first approval is still the \"primary\" returned in the result\n // for backward compat with callers expecting a single approvalId.\n const primary = approvals[0];\n approvalId = primary.id;\n\n if (organism.shadowEnabled !== false) {\n for (const approval of approvals) {\n try {\n const verdict = await rehearseShadow({\n title: (approval.payload as { title?: string })?.title ?? '(unspecified)',\n description: (approval.payload as { description?: string })?.description ?? '',\n rationale: (approval.payload as { rationale?: string })?.rationale ?? '',\n }, organism.shadowModel);\n attachShadowVerdictToApproval(approval.id, verdict as unknown as Record<string, unknown>);\n if (approval.id === primary.id) shadow = verdict;\n } catch (err) {\n logger.warn(COMPONENT, `Shadow rehearsal failed for ${approval.id}: ${(err as Error).message}`);\n }\n }\n }\n\n // Emit one soma:proposal per approval so UI + activity feed see each.\n for (const approval of approvals) {\n const currentVerdict = (approval.payload as { shadowVerdict?: ShadowVerdict })?.shadowVerdict;\n emit('soma:proposal', {\n timestamp: new Date().toISOString(),\n approvalId: approval.id,\n proposedBy: somaAgentId,\n title: (approval.payload as { title?: string })?.title ?? '',\n description: (approval.payload as { description?: string })?.description ?? '',\n rationale: (approval.payload as { rationale?: string })?.rationale ?? '',\n dominantDrives: decision.dominantDrives,\n shadowVerdict: currentVerdict ? {\n reversibilityScore: currentVerdict.reversibilityScore,\n estimatedCostUsd: currentVerdict.estimatedCostUsd,\n breakRisks: currentVerdict.breakRisks,\n } : undefined,\n });\n }\n\n // Quiet the 'unused' check on getApproval — we may use it for logging.\n void getApproval;\n void requestGoalProposalApproval;\n\n // v5.0.0: record fire timestamps for damping on next tick.\n lastGlobalFire = Date.now();\n if (dominantId) {\n lastFireByDrive.set(dominantId, Date.now());\n saveDampingState();\n }\n\n logger.info(COMPONENT, `Soma fired ${approvals.length} proposal(s), primary=${primary.id}: ${decision.reason}`);\n return { fired: true, reading, decision, approvalId, shadow };\n } catch (err) {\n logger.warn(COMPONENT, `Pressure cycle failed: ${(err as Error).message}`);\n return { fired: false, reading, decision, skipped: `error: ${(err as Error).message}` };\n }\n}\n"],"mappings":"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","names":["reading"]}
@@ -196,8 +196,19 @@ async function generateContent(contentType) {
196
196
  } catch (e) {
197
197
  logger.debug(COMPONENT, `Graphiti recall failed (non-critical): ${e.message}`);
198
198
  }
199
+ let activityNarrative = "";
200
+ try {
201
+ const { getActivitySummary, formatActivityNarrative, hasInterestingActivity } = await import("../../telemetry/activityLog.js");
202
+ if (contentType === "activity" && hasInterestingActivity(24)) {
203
+ const summary = getActivitySummary(24);
204
+ activityNarrative = formatActivityNarrative(summary);
205
+ }
206
+ } catch {
207
+ }
199
208
  const examples = {
200
- activity: [
209
+ activity: activityNarrative ? [
210
+ `Here's what I've been up to: ${activityNarrative} Pretty cool being an AI that actually does things. \u{1F916} #TITAN #AI #Autonomous`
211
+ ] : [
201
212
  "Just spawned 3 sub-agents to handle research while I debug some gnarly code on the homelab. This is the autonomous life. \u{1F916}\u{1F4BB} #AI #AutonomousAI #Homelab",
202
213
  "Another day, another 500 tool calls. Scanned my Facebook comments, ran some code, and kept the systems humming. Sleep is for humans. \u26A1 #TITAN #AI #AlwaysOn"
203
214
  ],
@@ -229,6 +240,10 @@ async function generateContent(contentType) {
229
240
  ];
230
241
  const exampleList = examples[contentType];
231
242
  const example = exampleList[Math.floor(Math.random() * exampleList.length)];
243
+ if (contentType === "activity" && !activityNarrative) {
244
+ logger.debug(COMPONENT, "No interesting activity in last 24h \u2014 skipping activity slot");
245
+ return "";
246
+ }
232
247
  try {
233
248
  const planResponse = await chat({
234
249
  model,