zora-agent 0.9.4 → 0.9.6

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 (245) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +103 -92
  3. package/dist/cli/audit-commands.d.ts.map +1 -1
  4. package/dist/cli/audit-commands.js +3 -1
  5. package/dist/cli/audit-commands.js.map +1 -1
  6. package/dist/cli/daemon.js +86 -28
  7. package/dist/cli/daemon.js.map +1 -1
  8. package/dist/cli/edit-commands.d.ts.map +1 -1
  9. package/dist/cli/edit-commands.js +3 -1
  10. package/dist/cli/edit-commands.js.map +1 -1
  11. package/dist/cli/hook-commands.d.ts +9 -0
  12. package/dist/cli/hook-commands.d.ts.map +1 -0
  13. package/dist/cli/hook-commands.js +106 -0
  14. package/dist/cli/hook-commands.js.map +1 -0
  15. package/dist/cli/index.js +87 -35
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/init-command.d.ts.map +1 -1
  18. package/dist/cli/init-command.js +108 -9
  19. package/dist/cli/init-command.js.map +1 -1
  20. package/dist/cli/memory-commands.d.ts +1 -1
  21. package/dist/cli/memory-commands.d.ts.map +1 -1
  22. package/dist/cli/memory-commands.js +213 -1
  23. package/dist/cli/memory-commands.js.map +1 -1
  24. package/dist/cli/presets.d.ts.map +1 -1
  25. package/dist/cli/presets.js +2 -1
  26. package/dist/cli/presets.js.map +1 -1
  27. package/dist/cli/skill-commands.d.ts.map +1 -1
  28. package/dist/cli/skill-commands.js +4 -2
  29. package/dist/cli/skill-commands.js.map +1 -1
  30. package/dist/cli/steer-commands.d.ts.map +1 -1
  31. package/dist/cli/steer-commands.js +6 -4
  32. package/dist/cli/steer-commands.js.map +1 -1
  33. package/dist/cli/team-commands.d.ts.map +1 -1
  34. package/dist/cli/team-commands.js +3 -1
  35. package/dist/cli/team-commands.js.map +1 -1
  36. package/dist/config/defaults.d.ts.map +1 -1
  37. package/dist/config/defaults.js +12 -2
  38. package/dist/config/defaults.js.map +1 -1
  39. package/dist/config/loader.d.ts +23 -0
  40. package/dist/config/loader.d.ts.map +1 -1
  41. package/dist/config/loader.js +64 -3
  42. package/dist/config/loader.js.map +1 -1
  43. package/dist/config/policy-loader.d.ts +14 -0
  44. package/dist/config/policy-loader.d.ts.map +1 -1
  45. package/dist/config/policy-loader.js +33 -0
  46. package/dist/config/policy-loader.js.map +1 -1
  47. package/dist/dashboard/frontend/dist/assets/index-BcOGj1EF.css +1 -0
  48. package/dist/dashboard/frontend/dist/assets/index-BtiFO9YN.js +261 -0
  49. package/dist/dashboard/frontend/dist/assets/index-Cfjy5acU.css +1 -0
  50. package/dist/dashboard/frontend/dist/assets/index-D41hcjgc.js +253 -0
  51. package/dist/dashboard/frontend/dist/assets/index-D83BawFd.css +1 -0
  52. package/dist/dashboard/frontend/dist/assets/index-DAODjoxu.css +1 -0
  53. package/dist/dashboard/frontend/dist/assets/index-DB-Eu5oV.js +253 -0
  54. package/dist/dashboard/frontend/dist/assets/index-W0VVEDu6.js +253 -0
  55. package/dist/dashboard/frontend/dist/index.html +17 -0
  56. package/dist/dashboard/server.d.ts +19 -2
  57. package/dist/dashboard/server.d.ts.map +1 -1
  58. package/dist/dashboard/server.js +121 -20
  59. package/dist/dashboard/server.js.map +1 -1
  60. package/dist/hooks/hook-runner.d.ts +55 -0
  61. package/dist/hooks/hook-runner.d.ts.map +1 -0
  62. package/dist/hooks/hook-runner.js +120 -0
  63. package/dist/hooks/hook-runner.js.map +1 -0
  64. package/dist/hooks/hook-types.d.ts +82 -0
  65. package/dist/hooks/hook-types.d.ts.map +1 -0
  66. package/dist/hooks/hook-types.js +20 -0
  67. package/dist/hooks/hook-types.js.map +1 -0
  68. package/dist/hooks/index.d.ts +6 -0
  69. package/dist/hooks/index.d.ts.map +1 -0
  70. package/dist/hooks/index.js +6 -0
  71. package/dist/hooks/index.js.map +1 -0
  72. package/dist/memory/context-compressor.d.ts +108 -0
  73. package/dist/memory/context-compressor.d.ts.map +1 -0
  74. package/dist/memory/context-compressor.js +307 -0
  75. package/dist/memory/context-compressor.js.map +1 -0
  76. package/dist/memory/index.d.ts +1 -0
  77. package/dist/memory/index.d.ts.map +1 -1
  78. package/dist/memory/index.js +1 -0
  79. package/dist/memory/index.js.map +1 -1
  80. package/dist/memory/memory-manager.d.ts +88 -4
  81. package/dist/memory/memory-manager.d.ts.map +1 -1
  82. package/dist/memory/memory-manager.js +299 -7
  83. package/dist/memory/memory-manager.js.map +1 -1
  84. package/dist/memory/observation-store.d.ts +75 -0
  85. package/dist/memory/observation-store.d.ts.map +1 -0
  86. package/dist/memory/observation-store.js +162 -0
  87. package/dist/memory/observation-store.js.map +1 -0
  88. package/dist/memory/observer-worker.d.ts +34 -0
  89. package/dist/memory/observer-worker.d.ts.map +1 -0
  90. package/dist/memory/observer-worker.js +161 -0
  91. package/dist/memory/observer-worker.js.map +1 -0
  92. package/dist/memory/reflector-worker.d.ts +40 -0
  93. package/dist/memory/reflector-worker.d.ts.map +1 -0
  94. package/dist/memory/reflector-worker.js +185 -0
  95. package/dist/memory/reflector-worker.js.map +1 -0
  96. package/dist/memory/salience-scorer.d.ts +16 -6
  97. package/dist/memory/salience-scorer.d.ts.map +1 -1
  98. package/dist/memory/salience-scorer.js +42 -22
  99. package/dist/memory/salience-scorer.js.map +1 -1
  100. package/dist/memory/structured-memory.d.ts +36 -1
  101. package/dist/memory/structured-memory.d.ts.map +1 -1
  102. package/dist/memory/structured-memory.js +207 -8
  103. package/dist/memory/structured-memory.js.map +1 -1
  104. package/dist/memory/token-estimator.d.ts +31 -0
  105. package/dist/memory/token-estimator.d.ts.map +1 -0
  106. package/dist/memory/token-estimator.js +77 -0
  107. package/dist/memory/token-estimator.js.map +1 -0
  108. package/dist/memory/validation-pipeline.d.ts +37 -0
  109. package/dist/memory/validation-pipeline.d.ts.map +1 -0
  110. package/dist/memory/validation-pipeline.js +106 -0
  111. package/dist/memory/validation-pipeline.js.map +1 -0
  112. package/dist/orchestrator/auth-monitor.d.ts.map +1 -1
  113. package/dist/orchestrator/auth-monitor.js +3 -1
  114. package/dist/orchestrator/auth-monitor.js.map +1 -1
  115. package/dist/orchestrator/execution-loop.d.ts +23 -0
  116. package/dist/orchestrator/execution-loop.d.ts.map +1 -1
  117. package/dist/orchestrator/execution-loop.js +60 -19
  118. package/dist/orchestrator/execution-loop.js.map +1 -1
  119. package/dist/orchestrator/failover-controller.d.ts +26 -2
  120. package/dist/orchestrator/failover-controller.d.ts.map +1 -1
  121. package/dist/orchestrator/failover-controller.js +143 -23
  122. package/dist/orchestrator/failover-controller.js.map +1 -1
  123. package/dist/orchestrator/orchestrator.d.ts +70 -7
  124. package/dist/orchestrator/orchestrator.d.ts.map +1 -1
  125. package/dist/orchestrator/orchestrator.js +416 -92
  126. package/dist/orchestrator/orchestrator.js.map +1 -1
  127. package/dist/orchestrator/retry-queue.d.ts.map +1 -1
  128. package/dist/orchestrator/retry-queue.js +24 -9
  129. package/dist/orchestrator/retry-queue.js.map +1 -1
  130. package/dist/orchestrator/router.d.ts +16 -1
  131. package/dist/orchestrator/router.d.ts.map +1 -1
  132. package/dist/orchestrator/router.js +79 -20
  133. package/dist/orchestrator/router.js.map +1 -1
  134. package/dist/orchestrator/session-manager.d.ts +26 -1
  135. package/dist/orchestrator/session-manager.d.ts.map +1 -1
  136. package/dist/orchestrator/session-manager.js +88 -4
  137. package/dist/orchestrator/session-manager.js.map +1 -1
  138. package/dist/providers/circuit-breaker.d.ts +78 -0
  139. package/dist/providers/circuit-breaker.d.ts.map +1 -0
  140. package/dist/providers/circuit-breaker.js +129 -0
  141. package/dist/providers/circuit-breaker.js.map +1 -0
  142. package/dist/providers/claude-provider.d.ts +27 -11
  143. package/dist/providers/claude-provider.d.ts.map +1 -1
  144. package/dist/providers/claude-provider.js +161 -46
  145. package/dist/providers/claude-provider.js.map +1 -1
  146. package/dist/providers/gemini-provider.d.ts +9 -1
  147. package/dist/providers/gemini-provider.d.ts.map +1 -1
  148. package/dist/providers/gemini-provider.js +97 -48
  149. package/dist/providers/gemini-provider.js.map +1 -1
  150. package/dist/providers/index.d.ts +1 -0
  151. package/dist/providers/index.d.ts.map +1 -1
  152. package/dist/providers/index.js +1 -0
  153. package/dist/providers/index.js.map +1 -1
  154. package/dist/providers/ollama-provider.d.ts +7 -0
  155. package/dist/providers/ollama-provider.d.ts.map +1 -1
  156. package/dist/providers/ollama-provider.js +89 -18
  157. package/dist/providers/ollama-provider.js.map +1 -1
  158. package/dist/routines/heartbeat.d.ts +10 -2
  159. package/dist/routines/heartbeat.d.ts.map +1 -1
  160. package/dist/routines/heartbeat.js +42 -5
  161. package/dist/routines/heartbeat.js.map +1 -1
  162. package/dist/routines/routine-manager.d.ts.map +1 -1
  163. package/dist/routines/routine-manager.js +22 -15
  164. package/dist/routines/routine-manager.js.map +1 -1
  165. package/dist/security/audit-logger.d.ts.map +1 -1
  166. package/dist/security/audit-logger.js +5 -7
  167. package/dist/security/audit-logger.js.map +1 -1
  168. package/dist/security/policy-engine.d.ts +28 -17
  169. package/dist/security/policy-engine.d.ts.map +1 -1
  170. package/dist/security/policy-engine.js +42 -185
  171. package/dist/security/policy-engine.js.map +1 -1
  172. package/dist/security/policy-serializer.d.ts +19 -0
  173. package/dist/security/policy-serializer.d.ts.map +1 -0
  174. package/dist/security/policy-serializer.js +100 -0
  175. package/dist/security/policy-serializer.js.map +1 -0
  176. package/dist/security/shell-validator.d.ts +42 -0
  177. package/dist/security/shell-validator.d.ts.map +1 -0
  178. package/dist/security/shell-validator.js +231 -0
  179. package/dist/security/shell-validator.js.map +1 -0
  180. package/dist/skills/index.d.ts +1 -0
  181. package/dist/skills/index.d.ts.map +1 -1
  182. package/dist/skills/index.js +1 -0
  183. package/dist/skills/index.js.map +1 -1
  184. package/dist/skills/skill-loader.d.ts +38 -2
  185. package/dist/skills/skill-loader.d.ts.map +1 -1
  186. package/dist/skills/skill-loader.js +83 -2
  187. package/dist/skills/skill-loader.js.map +1 -1
  188. package/dist/skills/subagent-loader.d.ts +66 -0
  189. package/dist/skills/subagent-loader.d.ts.map +1 -0
  190. package/dist/skills/subagent-loader.js +143 -0
  191. package/dist/skills/subagent-loader.js.map +1 -0
  192. package/dist/steering/flag-manager.d.ts +20 -0
  193. package/dist/steering/flag-manager.d.ts.map +1 -1
  194. package/dist/steering/flag-manager.js +94 -11
  195. package/dist/steering/flag-manager.js.map +1 -1
  196. package/dist/steering/steering-manager.d.ts +11 -0
  197. package/dist/steering/steering-manager.d.ts.map +1 -1
  198. package/dist/steering/steering-manager.js +23 -0
  199. package/dist/steering/steering-manager.js.map +1 -1
  200. package/dist/steering/telegram-gateway.d.ts +4 -1
  201. package/dist/steering/telegram-gateway.d.ts.map +1 -1
  202. package/dist/steering/telegram-gateway.js +49 -10
  203. package/dist/steering/telegram-gateway.js.map +1 -1
  204. package/dist/teams/bridge-watchdog.d.ts.map +1 -1
  205. package/dist/teams/bridge-watchdog.js +5 -3
  206. package/dist/teams/bridge-watchdog.js.map +1 -1
  207. package/dist/teams/gemini-bridge.d.ts.map +1 -1
  208. package/dist/teams/gemini-bridge.js +9 -4
  209. package/dist/teams/gemini-bridge.js.map +1 -1
  210. package/dist/tools/index.d.ts +2 -0
  211. package/dist/tools/index.d.ts.map +1 -1
  212. package/dist/tools/index.js +2 -0
  213. package/dist/tools/index.js.map +1 -1
  214. package/dist/tools/memory-tools.d.ts +16 -0
  215. package/dist/tools/memory-tools.d.ts.map +1 -0
  216. package/dist/tools/memory-tools.js +207 -0
  217. package/dist/tools/memory-tools.js.map +1 -0
  218. package/dist/tools/notifications.d.ts.map +1 -1
  219. package/dist/tools/notifications.js +3 -1
  220. package/dist/tools/notifications.js.map +1 -1
  221. package/dist/tools/tool-factory.d.ts +36 -0
  222. package/dist/tools/tool-factory.d.ts.map +1 -0
  223. package/dist/tools/tool-factory.js +55 -0
  224. package/dist/tools/tool-factory.js.map +1 -0
  225. package/dist/types.d.ts +205 -1
  226. package/dist/types.d.ts.map +1 -1
  227. package/dist/types.js +47 -1
  228. package/dist/types.js.map +1 -1
  229. package/dist/utils/errors.d.ts +21 -0
  230. package/dist/utils/errors.d.ts.map +1 -0
  231. package/dist/utils/errors.js +29 -0
  232. package/dist/utils/errors.js.map +1 -0
  233. package/dist/utils/event-filter.d.ts +25 -0
  234. package/dist/utils/event-filter.d.ts.map +1 -0
  235. package/dist/utils/event-filter.js +61 -0
  236. package/dist/utils/event-filter.js.map +1 -0
  237. package/dist/utils/logger.d.ts +33 -36
  238. package/dist/utils/logger.d.ts.map +1 -1
  239. package/dist/utils/logger.js +60 -130
  240. package/dist/utils/logger.js.map +1 -1
  241. package/dist/utils/validate-job-id.d.ts +6 -0
  242. package/dist/utils/validate-job-id.d.ts.map +1 -0
  243. package/dist/utils/validate-job-id.js +10 -0
  244. package/dist/utils/validate-job-id.js.map +1 -0
  245. package/package.json +12 -3
@@ -13,19 +13,29 @@ import crypto from 'node:crypto';
13
13
  import path from 'node:path';
14
14
  import os from 'node:os';
15
15
  import fs from 'node:fs';
16
+ import { HookRunner } from '../hooks/hook-runner.js';
16
17
  import { Router } from './router.js';
17
18
  import { FailoverController } from './failover-controller.js';
18
19
  import { RetryQueue } from './retry-queue.js';
19
20
  import { AuthMonitor } from './auth-monitor.js';
20
- import { SessionManager } from './session-manager.js';
21
- import { ExecutionLoop } from './execution-loop.js';
21
+ import { SessionManager, BufferedSessionWriter } from './session-manager.js';
22
+ import { ExecutionLoop, defaultTransformContext } from './execution-loop.js';
22
23
  import { SteeringManager } from '../steering/steering-manager.js';
23
24
  import { MemoryManager } from '../memory/memory-manager.js';
25
+ import { ExtractionPipeline } from '../memory/extraction-pipeline.js';
26
+ import { createMemoryTools } from '../tools/memory-tools.js';
27
+ import { ValidationPipeline } from '../memory/validation-pipeline.js';
28
+ import { ContextCompressor } from '../memory/context-compressor.js';
29
+ import { ObservationStore } from '../memory/observation-store.js';
24
30
  import { HeartbeatSystem } from '../routines/heartbeat.js';
25
31
  import { RoutineManager } from '../routines/routine-manager.js';
26
32
  import { NotificationTools } from '../tools/notifications.js';
27
33
  import { PolicyEngine } from '../security/policy-engine.js';
28
34
  import { IntentCapsuleManager } from '../security/intent-capsule.js';
35
+ import { LeakDetector } from '../security/leak-detector.js';
36
+ import { sanitizeInput } from '../security/prompt-defense.js';
37
+ import { createLogger } from '../utils/logger.js';
38
+ const log = createLogger('orchestrator');
29
39
  export class Orchestrator {
30
40
  _config;
31
41
  _policy;
@@ -43,12 +53,22 @@ export class Orchestrator {
43
53
  _notifications;
44
54
  // Security
45
55
  _intentCapsuleManager;
56
+ _leakDetector;
46
57
  // Background systems
47
58
  _heartbeatSystem = null;
48
59
  _routineManager = null;
60
+ // Memory tools
61
+ _validationPipeline;
62
+ // ORCH-12: Lifecycle hooks
63
+ _hookRunner = new HookRunner();
64
+ // ORCH-14: Context transform callback
65
+ _transformContext = defaultTransformContext;
66
+ // Context compression
67
+ _observationStore;
49
68
  // Background intervals
50
69
  _authCheckTimeout = null;
51
70
  _retryPollTimeout = null;
71
+ _consolidationTimeout = null;
52
72
  _booted = false;
53
73
  constructor(options) {
54
74
  this._config = options.config;
@@ -58,6 +78,20 @@ export class Orchestrator {
58
78
  }
59
79
  /**
60
80
  * Boots all subsystems and starts background loops.
81
+ *
82
+ * Initialization order:
83
+ * 1. PolicyEngine + IntentCapsuleManager (security layer).
84
+ * 2. SessionManager (event persistence).
85
+ * 3. SteeringManager (human-in-the-loop).
86
+ * 4. MemoryManager (context injection).
87
+ * 5. Router (provider selection).
88
+ * 6. FailoverController (error recovery).
89
+ * 7. RetryQueue (deferred retry).
90
+ * 8. AuthMonitor (periodic auth checks every 5 min).
91
+ * 9. HeartbeatSystem + RoutineManager (scheduled tasks).
92
+ *
93
+ * Background loops use self-rescheduling setTimeout (not setInterval)
94
+ * to avoid overlapping async executions.
61
95
  */
62
96
  async boot() {
63
97
  if (this._booted)
@@ -69,11 +103,17 @@ export class Orchestrator {
69
103
  // ASI01: Create IntentCapsuleManager with per-session signing key
70
104
  this._intentCapsuleManager = new IntentCapsuleManager(crypto.randomBytes(32).toString('hex'));
71
105
  this._policyEngine.setIntentCapsuleManager(this._intentCapsuleManager);
106
+ // SEC-03: Wire LeakDetector for scanning tool outputs
107
+ this._leakDetector = new LeakDetector();
72
108
  this._sessionManager = new SessionManager(this._baseDir);
73
109
  this._steeringManager = new SteeringManager(this._baseDir);
74
110
  await this._steeringManager.init();
75
111
  this._memoryManager = new MemoryManager(this._config.memory, this._baseDir);
76
112
  await this._memoryManager.init();
113
+ this._validationPipeline = new ValidationPipeline();
114
+ // Initialize observation store for context compression
115
+ this._observationStore = new ObservationStore(path.join(this._baseDir, 'memory', 'observations'));
116
+ await this._observationStore.init();
77
117
  // R2: Wire Router
78
118
  this._router = new Router({
79
119
  providers: this._providers,
@@ -99,7 +139,7 @@ export class Orchestrator {
99
139
  await this._authMonitor.checkAll();
100
140
  }
101
141
  catch (err) {
102
- console.error('[Orchestrator] AuthMonitor check failed:', err);
142
+ log.error({ err }, 'AuthMonitor check failed');
103
143
  }
104
144
  scheduleAuthCheck();
105
145
  }, 5 * 60 * 1000);
@@ -116,13 +156,13 @@ export class Orchestrator {
116
156
  await this._retryQueue.remove(task.jobId);
117
157
  }
118
158
  catch (err) {
119
- console.error(`[Orchestrator] Retry failed for ${task.jobId}:`, err);
159
+ log.error({ jobId: task.jobId, err }, 'Retry failed');
120
160
  // Leave task in queue for next poll cycle
121
161
  }
122
162
  }
123
163
  }
124
164
  catch (err) {
125
- console.error('[Orchestrator] RetryQueue poll failed:', err);
165
+ log.error({ err }, 'RetryQueue poll failed');
126
166
  }
127
167
  scheduleRetryPoll();
128
168
  }, 30 * 1000);
@@ -148,6 +188,31 @@ export class Orchestrator {
148
188
  maxCostTier: opts.maxCostTier,
149
189
  }), this._baseDir);
150
190
  await this._routineManager.init();
191
+ // Schedule daily note consolidation (check once per day)
192
+ const scheduleConsolidation = () => {
193
+ this._consolidationTimeout = setTimeout(async () => {
194
+ try {
195
+ const count = await this._memoryManager.consolidateDailyNotes(7);
196
+ if (count > 0) {
197
+ log.info({ consolidated: count }, 'Daily notes consolidated');
198
+ }
199
+ }
200
+ catch (err) {
201
+ log.warn({ err }, 'Daily note consolidation failed');
202
+ }
203
+ scheduleConsolidation();
204
+ }, 24 * 60 * 60 * 1000); // 24 hours
205
+ };
206
+ // Run first check shortly after boot (30 seconds), then daily
207
+ this._consolidationTimeout = setTimeout(async () => {
208
+ try {
209
+ await this._memoryManager.consolidateDailyNotes(7);
210
+ }
211
+ catch (err) {
212
+ log.warn({ err }, 'Initial daily note consolidation failed');
213
+ }
214
+ scheduleConsolidation();
215
+ }, 30 * 1000);
151
216
  this._booted = true;
152
217
  }
153
218
  /**
@@ -165,6 +230,10 @@ export class Orchestrator {
165
230
  clearTimeout(this._retryPollTimeout);
166
231
  this._retryPollTimeout = null;
167
232
  }
233
+ if (this._consolidationTimeout) {
234
+ clearTimeout(this._consolidationTimeout);
235
+ this._consolidationTimeout = null;
236
+ }
168
237
  // Stop heartbeat and routines
169
238
  if (this._heartbeatSystem) {
170
239
  this._heartbeatSystem.stop();
@@ -177,16 +246,34 @@ export class Orchestrator {
177
246
  this._booted = false;
178
247
  }
179
248
  /**
180
- * Submits a task through the full orchestration pipeline:
181
- * 1. Load memory context (R6)
182
- * 2. Classify and route to provider (R2)
183
- * 3. Execute with event persistence (R8) and steering (R7)
184
- * 4. Handle failures with failover (R3) and retry (R5)
249
+ * Submits a task through the full orchestration pipeline.
250
+ *
251
+ * Pipeline stages:
252
+ * 1. Load memory context from MemoryManager (daily notes, long-term items).
253
+ * 2. Load SOUL.md identity file and build the system prompt with policy awareness hints.
254
+ * 3. Create a signed intent capsule for goal drift detection (ASI01).
255
+ * 4. Classify the task by complexity and resource type for routing.
256
+ * 5. Route to the best available provider via the Router.
257
+ * 6. Execute via _executeWithProvider, which handles event persistence,
258
+ * steering injection, failover, and retry queueing.
259
+ *
260
+ * @returns The final text result from the provider's 'done' event.
261
+ * @throws If no provider is available or all failover attempts fail.
185
262
  */
186
263
  async submitTask(options) {
187
264
  const jobId = options.jobId ?? `job_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
188
- // R6: Inject MemoryManager context systematically
189
- const memoryContext = await this._memoryManager.loadContext();
265
+ // Reset per-task state: ValidationPipeline rate limit is per-session, not per-orchestrator-lifetime.
266
+ // Without this, after MAX_SAVES_PER_SESSION saves across all tasks, memory_save permanently blocks.
267
+ this._validationPipeline.resetSession();
268
+ // MEM-05 / ORCH-07: Progressive memory context — lightweight index, not full dump.
269
+ // The LLM uses memory_search / recall_context tools for on-demand retrieval.
270
+ let memoryContext = [];
271
+ try {
272
+ memoryContext = await this._memoryManager.loadContext();
273
+ }
274
+ catch (err) {
275
+ log.warn({ err, jobId }, 'Memory context injection failed, continuing without memory');
276
+ }
190
277
  // Load SOUL.md for agent identity (fixes bug: file was created but never read)
191
278
  const soulPath = this._config.agent.identity.soul_file.replace(/^~/, os.homedir());
192
279
  let soulContent = '';
@@ -198,25 +285,57 @@ export class Orchestrator {
198
285
  catch {
199
286
  // SOUL.md missing or unreadable — use default identity
200
287
  }
288
+ // Create per-task context compressor if compression is enabled
289
+ let compressor = null;
290
+ if (this._config.memory?.compression?.enabled) {
291
+ const compressFn = async (prompt) => {
292
+ const compressLoop = new ExecutionLoop({
293
+ systemPrompt: 'You are a conversation observer. Compress messages into concise, dated observations. Respond with ONLY the observations.',
294
+ permissionMode: 'default',
295
+ cwd: process.cwd(),
296
+ maxTurns: 1,
297
+ model: this._config.memory.compression.model,
298
+ });
299
+ return compressLoop.run(prompt);
300
+ };
301
+ compressor = new ContextCompressor(this._config.memory.compression, this._observationStore, compressFn, jobId);
302
+ await compressor.loadExisting();
303
+ }
304
+ // Build cross-session context from observations
305
+ const crossSessionContext = compressor
306
+ ? compressor.buildContext().crossSessionContext
307
+ : '';
201
308
  // Build system prompt with policy awareness
202
- const systemPrompt = [
309
+ const systemPromptParts = [
203
310
  soulContent || 'You are Zora, a helpful autonomous agent.',
204
311
  '[SECURITY] You operate under a permission policy. Before planning any task,',
205
312
  'use the check_permissions tool to verify you have access to the paths and',
206
313
  'commands you need. If access is denied, tell the user what you need and why.',
207
314
  'Do NOT attempt actions without checking first.',
208
315
  ...memoryContext,
209
- ].join('\n\n');
316
+ ];
317
+ // Append cross-session observations if available
318
+ if (crossSessionContext) {
319
+ systemPromptParts.push(`[PRIOR SESSION CONTEXT]:\n${crossSessionContext}`);
320
+ }
321
+ const systemPrompt = systemPromptParts.join('\n\n');
322
+ // SEC-03: Scan user prompt for injection patterns (warn but don't block by default)
323
+ const sanitizedPrompt = sanitizeInput(options.prompt);
324
+ if (sanitizedPrompt !== options.prompt) {
325
+ log.warn({ jobId }, 'Prompt injection pattern detected in user input — sanitized');
326
+ }
210
327
  // ASI01: Create signed intent capsule for goal drift detection
211
328
  if (this._intentCapsuleManager) {
212
- this._intentCapsuleManager.createCapsule(options.prompt);
329
+ this._intentCapsuleManager.createCapsule(sanitizedPrompt);
213
330
  }
214
331
  // Classify task for routing
215
- const classification = this._router.classifyTask(options.prompt);
332
+ const classification = this._router.classifyTask(sanitizedPrompt);
333
+ // Build custom tools (permissions + memory tools + recall_context)
334
+ const customTools = this._createCustomTools();
216
335
  // Build task context
217
336
  const taskContext = {
218
337
  jobId,
219
- task: options.prompt,
338
+ task: sanitizedPrompt,
220
339
  requiredCapabilities: [],
221
340
  complexity: classification.complexity,
222
341
  resourceType: classification.resourceType,
@@ -226,117 +345,286 @@ export class Orchestrator {
226
345
  modelPreference: options.model,
227
346
  maxCostTier: options.maxCostTier,
228
347
  maxTurns: options.maxTurns,
348
+ customTools,
229
349
  canUseTool: this._policyEngine.createCanUseTool(),
230
350
  };
351
+ // ORCH-12: Run onTaskStart hooks (can modify context before routing)
352
+ const hookedContext = await this._hookRunner.runOnTaskStart(taskContext);
231
353
  // R2: Route to provider
232
354
  let selectedProvider;
233
355
  try {
234
- selectedProvider = await this._router.selectProvider(taskContext);
356
+ selectedProvider = await this._router.selectProvider(hookedContext);
235
357
  }
236
358
  catch (err) {
237
359
  throw new Error(`No provider available: ${err instanceof Error ? err.message : String(err)}`);
238
360
  }
239
- // Execute with the selected provider
240
- return this._executeWithProvider(selectedProvider, taskContext, options.onEvent);
361
+ // Execute with the selected provider (injectionDepth=0 for initial call)
362
+ return this._executeWithProvider(selectedProvider, hookedContext, options.onEvent, 0, 0, compressor);
241
363
  }
242
- /** Symbol used to mark errors that have already been through the failover path */
243
- static _FAILOVER_SENTINEL = Symbol.for('zora.failoverSentinel');
364
+ /** Tracks errors that have already been through the failover path */
365
+ static _failoverErrors = new WeakSet();
244
366
  /** Maximum depth of failover recursion to prevent unbounded re-execution */
245
367
  static MAX_FAILOVER_DEPTH = 3;
368
+ /** ORCH-16: Maximum depth of onTaskEnd follow-up injection loops */
369
+ static MAX_INJECTION_LOOPS = 3;
246
370
  /**
247
371
  * Executes a task with a specific provider, handling failover and event persistence.
372
+ *
373
+ * During execution, this method:
374
+ * - Persists every event to the SessionManager for crash recovery.
375
+ * - Polls SteeringManager after text/tool_result events, injecting any pending
376
+ * human steering messages into the event stream.
377
+ * - On error events: attempts failover via FailoverController. If failover
378
+ * succeeds, recurses with the new provider (incrementing failoverDepth).
379
+ * If failover fails, enqueues the task in the RetryQueue.
380
+ * - failoverDepth is capped at MAX_FAILOVER_DEPTH (3) to prevent unbounded recursion.
381
+ * - The _failoverErrors WeakSet prevents double-failover: errors already processed
382
+ * by the failover path are not re-triggered in the outer catch block.
248
383
  */
249
- async _executeWithProvider(provider, taskContext, onEvent, failoverDepth = 0) {
384
+ async _executeWithProvider(provider, taskContext, onEvent, failoverDepth = 0, injectionDepth = 0, compressor) {
250
385
  let result = '';
386
+ let eventsSinceLastTick = 0;
387
+ const TICK_INTERVAL = 10; // Check compression thresholds every N events
388
+ // Event batching: buffer session writes, flush every 500ms or on done/error.
389
+ // Wrapped in try/finally to ensure close() runs on ALL exit paths including failover.
390
+ const bufferedWriter = new BufferedSessionWriter(this._sessionManager, taskContext.jobId, 500);
251
391
  try {
252
- // Execute via the provider's async generator
253
- for await (const event of provider.execute(taskContext)) {
254
- // R8: Persist events to SessionManager
255
- await this._sessionManager.appendEvent(taskContext.jobId, event);
256
- // R7: Poll SteeringManager during execution
257
- if (event.type === 'text' || event.type === 'tool_result') {
258
- const pendingMessages = await this._steeringManager.getPendingMessages(taskContext.jobId);
259
- for (const msg of pendingMessages) {
260
- // Inject steering as an event
261
- const steerEvent = {
262
- type: 'steering',
263
- timestamp: new Date(),
264
- content: { text: msg.message, source: msg.source, author: msg.author },
265
- };
266
- await this._sessionManager.appendEvent(taskContext.jobId, steerEvent);
267
- taskContext.history.push(steerEvent);
268
- if (onEvent)
269
- onEvent(steerEvent);
270
- // Archive the processed message
271
- await this._steeringManager.archiveMessage(taskContext.jobId, msg.id);
392
+ try {
393
+ // Execute via the provider's async generator
394
+ for await (const event of provider.execute(taskContext)) {
395
+ // R8: Persist events via buffered writer (batched disk I/O)
396
+ bufferedWriter.append(event);
397
+ // Feed events to context compressor for rolling compression
398
+ if (compressor) {
399
+ compressor.ingest(event);
400
+ eventsSinceLastTick++;
401
+ if (eventsSinceLastTick >= TICK_INTERVAL) {
402
+ eventsSinceLastTick = 0;
403
+ // tick() is async but we don't await — compression runs in background
404
+ compressor.tick().catch(err => {
405
+ log.warn({ err, jobId: taskContext.jobId }, 'Context compressor tick failed');
406
+ });
407
+ }
272
408
  }
273
- }
274
- // Notify caller
275
- if (onEvent)
276
- onEvent(event);
277
- // Track history for failover handoff
278
- taskContext.history.push(event);
279
- // Capture result text
280
- if (event.type === 'done') {
281
- result = event.content.text ?? '';
282
- }
283
- // Handle errors — trigger failover (R3)
284
- if (event.type === 'error') {
285
- const errorContent = event.content;
286
- const error = new Error(errorContent.message ?? 'Unknown provider error');
287
- // Guard: skip failover if depth exceeded
288
- if (failoverDepth >= Orchestrator.MAX_FAILOVER_DEPTH) {
409
+ // SEC-03: Scan tool outputs for leaked secrets (warn, don't strip)
410
+ if (event.type === 'tool_result') {
411
+ const toolResultContent = event.content;
412
+ const resultText = typeof toolResultContent.result === 'string'
413
+ ? toolResultContent.result
414
+ : JSON.stringify(toolResultContent.result ?? '');
415
+ const leaks = this._leakDetector.scan(resultText);
416
+ if (leaks.length > 0) {
417
+ log.warn({ jobId: taskContext.jobId, toolCallId: toolResultContent.toolCallId, leaks: leaks.map(l => ({ pattern: l.pattern, severity: l.severity })) }, 'Potential secret leak detected in tool output');
418
+ }
419
+ }
420
+ // SEC-03: Scan tool call arguments for leaked secrets
421
+ if (event.type === 'tool_call') {
422
+ const toolCallContent = event.content;
423
+ const argsText = JSON.stringify(toolCallContent.arguments ?? {});
424
+ const leaks = this._leakDetector.scan(argsText);
425
+ if (leaks.length > 0) {
426
+ log.warn({ jobId: taskContext.jobId, tool: toolCallContent.tool, leaks: leaks.map(l => ({ pattern: l.pattern, severity: l.severity })) }, 'Potential secret leak detected in tool call arguments');
427
+ }
428
+ }
429
+ // R7: Poll SteeringManager with debouncing (max once per 2 seconds)
430
+ if (event.type === 'text' || event.type === 'tool_result') {
431
+ const pendingMessages = await this._steeringManager.cachedGetPendingMessages(taskContext.jobId, 2000);
432
+ for (const msg of pendingMessages) {
433
+ // Inject steering as an event
434
+ const steerEvent = {
435
+ type: 'steering',
436
+ timestamp: new Date(),
437
+ content: { text: msg.type === 'steer' ? msg.message : `[${msg.type}]`, source: msg.source, author: msg.author },
438
+ };
439
+ bufferedWriter.append(steerEvent);
440
+ taskContext.history.push(steerEvent);
441
+ if (onEvent)
442
+ onEvent(steerEvent);
443
+ // Archive the processed message and invalidate cache
444
+ await this._steeringManager.archiveMessage(taskContext.jobId, msg.id);
445
+ this._steeringManager.invalidatePendingCache(taskContext.jobId);
446
+ }
447
+ }
448
+ // Notify caller
449
+ if (onEvent)
450
+ onEvent(event);
451
+ // Track history for failover handoff
452
+ taskContext.history.push(event);
453
+ // Capture result text
454
+ if (event.type === 'done') {
455
+ result = event.content.text ?? '';
456
+ }
457
+ // Handle errors — trigger failover (R3)
458
+ if (event.type === 'error') {
459
+ const errorContent = event.content;
460
+ const error = new Error(errorContent.message ?? 'Unknown provider error');
461
+ // Guard: skip failover if depth exceeded
462
+ if (failoverDepth >= Orchestrator.MAX_FAILOVER_DEPTH) {
463
+ throw error;
464
+ }
465
+ // R3: Connect FailoverController to error path
466
+ const failoverResult = await this._failoverController.handleFailure(taskContext, provider, error);
467
+ if (failoverResult) {
468
+ // Re-execute with the failover provider (increment depth)
469
+ return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1, injectionDepth, compressor);
470
+ }
471
+ // R5: Enqueue for retry if no failover available
472
+ try {
473
+ await this._retryQueue.enqueue(taskContext, error.message, this._config.failover.max_retries);
474
+ }
475
+ catch {
476
+ // Max retries exceeded or enqueue failed
477
+ }
478
+ // Mark so the outer catch doesn't re-trigger failover
479
+ Orchestrator._failoverErrors.add(error);
289
480
  throw error;
290
481
  }
291
- // R3: Connect FailoverController to error path
292
- const failoverResult = await this._failoverController.handleFailure(taskContext, provider, error);
482
+ }
483
+ }
484
+ catch (err) {
485
+ // Skip failover for errors already marked by the failover path
486
+ const isFailoverError = err instanceof Error && Orchestrator._failoverErrors.has(err);
487
+ if (!isFailoverError && err instanceof Error && failoverDepth < Orchestrator.MAX_FAILOVER_DEPTH) {
488
+ // R3: Try failover on execution exceptions
489
+ const failoverResult = await this._failoverController.handleFailure(taskContext, provider, err);
293
490
  if (failoverResult) {
294
- // Re-execute with the failover provider (increment depth)
295
- return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1);
491
+ // Mark the error so downstream doesn't re-trigger failover
492
+ Orchestrator._failoverErrors.add(err);
493
+ return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1, injectionDepth, compressor);
296
494
  }
297
- // R5: Enqueue for retry if no failover available
495
+ // R5: Enqueue for retry
298
496
  try {
299
- await this._retryQueue.enqueue(taskContext, error.message, this._config.failover.max_retries);
497
+ await this._retryQueue.enqueue(taskContext, err.message, this._config.failover.max_retries);
300
498
  }
301
499
  catch {
302
- // Max retries exceeded or enqueue failed
500
+ // Max retries exceeded
303
501
  }
304
- // Mark so the outer catch doesn't re-trigger failover
305
- error[Orchestrator._FAILOVER_SENTINEL] = true;
306
- throw error;
307
502
  }
503
+ throw err;
308
504
  }
309
505
  }
310
- catch (err) {
311
- // Skip failover for errors already marked by the failover path
312
- const isFailoverError = err instanceof Error && err[Orchestrator._FAILOVER_SENTINEL];
313
- if (!isFailoverError && err instanceof Error && failoverDepth < Orchestrator.MAX_FAILOVER_DEPTH) {
314
- // R3: Try failover on execution exceptions
315
- const failoverResult = await this._failoverController.handleFailure(taskContext, provider, err);
316
- if (failoverResult) {
317
- // Mark the error so downstream doesn't re-trigger failover
318
- err[Orchestrator._FAILOVER_SENTINEL] = true;
319
- return this._executeWithProvider(failoverResult.nextProvider, taskContext, onEvent, failoverDepth + 1);
320
- }
321
- // R5: Enqueue for retry
322
- try {
323
- await this._retryQueue.enqueue(taskContext, err.message, this._config.failover.max_retries);
324
- }
325
- catch {
326
- // Max retries exceeded
327
- }
506
+ finally {
507
+ // Always close the buffered writer flushes remaining events and stops the timer.
508
+ // This runs on all exit paths: success, throw, and failover returns.
509
+ await bufferedWriter.close();
510
+ // Flush context compressor persist any remaining observations
511
+ if (compressor) {
512
+ await compressor.flush().catch(err => {
513
+ log.warn({ err, jobId: taskContext.jobId }, 'Context compressor flush failed');
514
+ });
328
515
  }
329
- throw err;
330
516
  }
331
517
  // Record completion in daily notes
332
518
  await this._memoryManager.appendDailyNote(`Completed task: ${taskContext.task}`);
519
+ // MEM-09: Async memory extraction after successful job completion
520
+ if (this._config.memory.auto_extract) {
521
+ this._runExtractionAsync(taskContext).catch(err => {
522
+ log.warn({ err, jobId: taskContext.jobId }, 'Post-job memory extraction failed');
523
+ });
524
+ }
525
+ // ORCH-12: Run onTaskEnd hooks (can inspect result, optionally trigger follow-up)
526
+ // ORCH-16: Guard against infinite follow-up injection loops
527
+ const endResult = await this._hookRunner.runOnTaskEnd(taskContext, result);
528
+ if (endResult.followUp) {
529
+ if (injectionDepth >= Orchestrator.MAX_INJECTION_LOOPS) {
530
+ log.warn({ jobId: taskContext.jobId, depth: injectionDepth, maxDepth: Orchestrator.MAX_INJECTION_LOOPS }, 'onTaskEnd follow-up injection loop capped — skipping follow-up');
531
+ }
532
+ else {
533
+ log.info({ jobId: taskContext.jobId, depth: injectionDepth + 1 }, 'onTaskEnd hook triggered follow-up task');
534
+ // Route through _executeWithProvider directly to preserve injectionDepth tracking.
535
+ // Re-route through the full submitTask pipeline except use incremented injectionDepth.
536
+ const followUpJobId = `${taskContext.jobId}_followup_${injectionDepth + 1}`;
537
+ // ORCH-14: Apply transformContext to prune history before follow-up
538
+ const transformedHistory = this._transformContext(taskContext.history, injectionDepth + 1);
539
+ const followUpCtx = {
540
+ ...taskContext,
541
+ jobId: followUpJobId,
542
+ task: endResult.followUp,
543
+ history: transformedHistory,
544
+ };
545
+ const hookedFollowUp = await this._hookRunner.runOnTaskStart(followUpCtx);
546
+ const followUpProvider = await this._router.selectProvider(hookedFollowUp);
547
+ return this._executeWithProvider(followUpProvider, hookedFollowUp, onEvent, 0, injectionDepth + 1);
548
+ }
549
+ }
333
550
  return result;
334
551
  }
552
+ /**
553
+ * MEM-09: Runs memory extraction asynchronously after job completion.
554
+ *
555
+ * Collects text events from the job history, passes them through
556
+ * ExtractionPipeline, deduplicates against existing items, and
557
+ * persists new items via StructuredMemory. Appends a daily note
558
+ * summarizing what was extracted.
559
+ *
560
+ * Runs fire-and-forget — errors are caught by the caller.
561
+ */
562
+ async _runExtractionAsync(taskContext) {
563
+ // Collect conversation text from job history
564
+ const messages = taskContext.history
565
+ .filter(e => e.type === 'text' || e.type === 'done')
566
+ .map(e => {
567
+ const content = e.content;
568
+ return content.text;
569
+ })
570
+ .filter(Boolean);
571
+ if (messages.length === 0) {
572
+ return; // Nothing to extract from
573
+ }
574
+ // Get existing categories for context
575
+ const categories = await this._memoryManager.getCategories();
576
+ const categoryNames = categories.map(c => c.category);
577
+ // Create extraction pipeline using the first available provider as the LLM
578
+ const extractFn = async (prompt) => {
579
+ const extractLoop = new ExecutionLoop({
580
+ systemPrompt: 'You extract structured memory items from conversations. Respond with ONLY a JSON array.',
581
+ permissionMode: 'default',
582
+ cwd: process.cwd(),
583
+ maxTurns: 1,
584
+ });
585
+ return extractLoop.run(prompt);
586
+ };
587
+ const pipeline = new ExtractionPipeline(extractFn);
588
+ const result = await pipeline.extract(messages, categoryNames);
589
+ if (result.errors.length > 0) {
590
+ log.debug({ errors: result.errors, jobId: taskContext.jobId }, 'Extraction had errors');
591
+ }
592
+ if (result.items.length === 0) {
593
+ return;
594
+ }
595
+ // Deduplicate against existing items
596
+ const existingItems = await this._memoryManager.structuredMemory.listItems();
597
+ const uniqueItems = pipeline.deduplicateItems(result.items, existingItems);
598
+ // Persist each new item
599
+ let savedCount = 0;
600
+ for (const item of uniqueItems) {
601
+ try {
602
+ await this._memoryManager.structuredMemory.createItem({
603
+ type: item.type,
604
+ summary: item.summary,
605
+ source: item.source || taskContext.jobId,
606
+ source_type: item.source_type,
607
+ tags: item.tags,
608
+ category: item.category,
609
+ });
610
+ savedCount++;
611
+ }
612
+ catch (err) {
613
+ log.debug({ err, item: item.summary }, 'Failed to save extracted memory item');
614
+ }
615
+ }
616
+ // Append daily note summarizing extraction
617
+ if (savedCount > 0) {
618
+ await this._memoryManager.appendDailyNote(`Extracted ${savedCount} memory item(s) from job ${taskContext.jobId}`);
619
+ }
620
+ log.info({ jobId: taskContext.jobId, extracted: result.items.length, saved: savedCount }, 'Memory extraction complete');
621
+ }
335
622
  /**
336
623
  * Creates custom tools available to the agent during execution.
624
+ * Includes: permission tools, memory tools (search/save/forget), recall_context.
337
625
  */
338
626
  _createCustomTools() {
339
- return [
627
+ const permissionTools = [
340
628
  {
341
629
  name: 'check_permissions',
342
630
  description: 'Check if you have access to specific paths or commands before executing. Use this during planning to verify your boundaries.',
@@ -383,9 +671,6 @@ export class Orchestrator {
383
671
  }
384
672
  }
385
673
  }
386
- // The actual approval flow happens outside (CLI prompt, dashboard, etc.)
387
- // For now, return the request details so the caller can present them to the user.
388
- // In the CLI, this will be intercepted by the onEvent callback.
389
674
  return {
390
675
  granted: false,
391
676
  pending: true,
@@ -395,6 +680,33 @@ export class Orchestrator {
395
680
  },
396
681
  },
397
682
  ];
683
+ // Wire existing memory tools (memory_search, memory_save, memory_forget)
684
+ const memoryTools = createMemoryTools(this._memoryManager, this._validationPipeline);
685
+ // Add recall_context tool for daily notes retrieval
686
+ const recallContextTool = {
687
+ name: 'recall_context',
688
+ description: 'Retrieve recent daily notes (rolling conversation summaries). ' +
689
+ 'Use this to get context from the past few days of agent activity.',
690
+ input_schema: {
691
+ type: 'object',
692
+ properties: {
693
+ days: {
694
+ type: 'number',
695
+ description: 'Number of recent days to retrieve (default: 3, max: 14).',
696
+ default: 3,
697
+ },
698
+ },
699
+ },
700
+ handler: async (input) => {
701
+ const days = Math.min(Math.max(input.days ?? 3, 1), 14);
702
+ const notes = await this._memoryManager.recallDailyNotes(days);
703
+ if (notes.length === 0) {
704
+ return { notes: [], message: 'No daily notes found for the requested period.' };
705
+ }
706
+ return { notes, count: notes.length, days };
707
+ },
708
+ };
709
+ return [...permissionTools, ...memoryTools, recallContextTool];
398
710
  }
399
711
  /**
400
712
  * Parse interval strings like "30m", "1h" to minutes.
@@ -447,6 +759,18 @@ export class Orchestrator {
447
759
  this._assertBooted();
448
760
  return this._policyEngine;
449
761
  }
762
+ /** ORCH-12: Access the hook runner for registering lifecycle hooks */
763
+ get hookRunner() {
764
+ return this._hookRunner;
765
+ }
766
+ /** ORCH-14: Set a custom context transform function */
767
+ set transformContext(fn) {
768
+ this._transformContext = fn;
769
+ }
770
+ /** ORCH-14: Get the current context transform function */
771
+ get transformContext() {
772
+ return this._transformContext;
773
+ }
450
774
  get config() {
451
775
  return this._config;
452
776
  }