openhermes 4.9.2 → 4.12.1

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 (85) hide show
  1. package/CONTEXT.md +7 -7
  2. package/ETHOS.md +2 -2
  3. package/README.md +34 -33
  4. package/bootstrap.ts +310 -160
  5. package/harness/agents/oh-planner.md +1 -1
  6. package/harness/agents/openhermes.md +27 -126
  7. package/harness/codex/AUTOPILOT.md +131 -23
  8. package/harness/codex/CHARTER.md +4 -5
  9. package/harness/lib/background/background.test.ts +216 -0
  10. package/harness/lib/background/index.ts +7 -0
  11. package/harness/lib/background/interfaces.ts +31 -0
  12. package/harness/lib/background/manager.ts +320 -0
  13. package/harness/lib/composer/compose.test.ts +179 -0
  14. package/harness/lib/composer/compose.ts +65 -0
  15. package/harness/lib/composer/fragments/01-identity.md +1 -0
  16. package/harness/lib/composer/fragments/02-delegation.md +7 -0
  17. package/harness/lib/composer/fragments/03-permissions.md +13 -0
  18. package/harness/lib/composer/fragments/04-task-flow.md +55 -0
  19. package/harness/lib/composer/fragments/05-confidence.md +5 -0
  20. package/harness/lib/composer/fragments/06-parallelization.md +17 -0
  21. package/harness/lib/composer/fragments/07-shell.md +41 -0
  22. package/harness/lib/composer/fragments/08-routing.md +8 -0
  23. package/harness/lib/composer/fragments/09-guardrails.md +25 -0
  24. package/harness/lib/composer/index.ts +1 -0
  25. package/harness/lib/guards/guard-config.ts +72 -0
  26. package/harness/lib/hooks/builtins/confidence-gate-hook.ts +68 -0
  27. package/harness/lib/hooks/builtins/delegation-depth-hook.ts +78 -0
  28. package/harness/lib/hooks/builtins/dynamic-route-hook.ts +99 -0
  29. package/harness/lib/hooks/builtins/error-recovery-hook.ts +107 -0
  30. package/harness/lib/hooks/builtins/memory-sync-hook.ts +73 -0
  31. package/harness/lib/hooks/builtins/next-route-hook.ts +24 -0
  32. package/harness/lib/hooks/builtins/plan-check-hook.ts +43 -0
  33. package/harness/lib/hooks/builtins/route-tracking-hook.ts +201 -0
  34. package/harness/lib/hooks/builtins/sanity-check-hook.ts +52 -0
  35. package/harness/lib/hooks/builtins/shell-detect-hook.ts +96 -0
  36. package/harness/lib/hooks/builtins/subagent-failure-hook.ts +93 -0
  37. package/harness/lib/hooks/hooks.test.ts +1092 -0
  38. package/harness/lib/hooks/index.ts +42 -0
  39. package/harness/lib/hooks/registry.ts +416 -0
  40. package/harness/lib/hooks/types.ts +119 -0
  41. package/harness/lib/memory/index.ts +18 -0
  42. package/harness/lib/memory/interfaces.ts +53 -0
  43. package/harness/lib/memory/memory-manager.ts +205 -0
  44. package/harness/lib/memory/memory.test.ts +485 -0
  45. package/harness/lib/memory/plan-store.ts +346 -0
  46. package/harness/lib/plans/plan-location.ts +134 -0
  47. package/harness/lib/recovery/handler.ts +243 -0
  48. package/harness/lib/recovery/index.ts +14 -0
  49. package/harness/lib/recovery/interfaces.ts +48 -0
  50. package/harness/lib/recovery/patterns.ts +149 -0
  51. package/harness/lib/recovery/recovery.test.ts +312 -0
  52. package/harness/lib/routing/index.ts +21 -0
  53. package/harness/lib/routing/route-guidance.ts +147 -0
  54. package/harness/lib/routing/route-resolver.ts +58 -0
  55. package/harness/lib/routing/routing.test.ts +195 -0
  56. package/harness/lib/routing/skill-frontmatter.ts +125 -0
  57. package/harness/lib/routing/types.ts +52 -0
  58. package/harness/lib/sanity/anomaly-tracker.ts +127 -0
  59. package/harness/lib/sanity/checker.ts +189 -0
  60. package/harness/lib/sanity/index.ts +13 -0
  61. package/harness/lib/sanity/interfaces.ts +24 -0
  62. package/harness/lib/sanity/sanity.test.ts +472 -0
  63. package/harness/lib/sync/file-watcher.ts +175 -0
  64. package/harness/lib/sync/index.ts +11 -0
  65. package/harness/lib/sync/interfaces.ts +27 -0
  66. package/harness/lib/sync/plan-sync.ts +533 -0
  67. package/harness/lib/sync/sync.test.ts +858 -0
  68. package/harness/skills/oh-fusion/DEEP.md +109 -86
  69. package/harness/skills/oh-fusion/SKILL.md +47 -33
  70. package/harness/skills/oh-init/DEEP.md +2 -2
  71. package/harness/skills/oh-manifest/SKILL.md +2 -1
  72. package/harness/skills/oh-plan-review/DEEP.md +1 -1
  73. package/harness/skills/oh-planner/DEEP.md +3 -3
  74. package/harness/skills/oh-review/DEEP.md +5 -3
  75. package/harness/skills/oh-review/SKILL.md +1 -0
  76. package/harness/skills/oh-ship/SKILL.md +1 -1
  77. package/harness/skills/oh-skill-craft/SKILL.md +1 -4
  78. package/package.json +53 -55
  79. package/tsconfig.json +1 -1
  80. package/harness/commands/oh-doctor.md +0 -205
  81. package/harness/commands/oh-log.md +0 -18
  82. package/harness/skills/oh-learn/DEEP.md +0 -44
  83. package/harness/skills/oh-learn/SKILL.md +0 -30
  84. package/scripts/count-tokens.mjs +0 -158
  85. package/scripts/oh-doctor.ps1 +0 -342
@@ -0,0 +1,243 @@
1
+ // RecoveryHandler — singleton that classifies errors against patterns,
2
+ // applies recovery actions, and tracks stats.
3
+
4
+ import type {
5
+ ErrorCategory,
6
+ ErrorContext,
7
+ RecoveryAction,
8
+ RecoveryActionType,
9
+ RecoveryRecord,
10
+ RecoveryStats,
11
+ } from "./interfaces.ts";
12
+ import { PATTERNS, escalateAction } from "./patterns.ts";
13
+
14
+ /**
15
+ * Represents the number of times we have *attempted recovery* for a
16
+ * given (sessionId, category) pair. Separate from the sub-agent's own
17
+ * attempt counter which is passed in ErrorContext.attempt.
18
+ */
19
+ interface CategoryAttemptState {
20
+ count: number;
21
+ }
22
+
23
+ export class RecoveryHandler {
24
+ private static instance: RecoveryHandler;
25
+
26
+ /** All recovery records keyed by sessionId then timestamp. */
27
+ private history: RecoveryRecord[] = [];
28
+
29
+ /** Track attempts per (sessionId + category) to enforce maxAttempts. */
30
+ private attemptTracker = new Map<string, CategoryAttemptState>();
31
+
32
+ /** Total number of recoveries that succeeded (fn completed without throwing). */
33
+ private successCount = 0;
34
+ private failureCount = 0;
35
+
36
+ private constructor() {}
37
+
38
+ /** Get the singleton instance. */
39
+ static getInstance(): RecoveryHandler {
40
+ if (!RecoveryHandler.instance) {
41
+ RecoveryHandler.instance = new RecoveryHandler();
42
+ }
43
+ return RecoveryHandler.instance;
44
+ }
45
+
46
+ /**
47
+ * Classify an error and return the appropriate recovery action.
48
+ * Returns the first matching pattern's action, or escalates if no match.
49
+ */
50
+ handleError(context: ErrorContext): RecoveryAction {
51
+ const message = context.error.message ?? String(context.error);
52
+
53
+ for (const entry of PATTERNS) {
54
+ if (entry.pattern.test(message)) {
55
+ const action = entry.getAction(context);
56
+ this.record(context, action);
57
+ return action;
58
+ }
59
+ }
60
+
61
+ // No pattern matched — escalate
62
+ const action = escalateAction(context);
63
+ this.record(context, action);
64
+ return action;
65
+ }
66
+
67
+ /**
68
+ * Wraps an async function with auto-recovery.
69
+ *
70
+ * On each throw:
71
+ * 1. Classify the error via handleError()
72
+ * 2. If action is "abort" | "escalate" | "skip" — rethrow immediately
73
+ * 3. If action is "compact" | "retry" — check maxAttempts, delay, retry
74
+ *
75
+ * If the function succeeds, increments successCount.
76
+ */
77
+ async withRecovery<T>(
78
+ sessionId: string,
79
+ fn: () => Promise<T>,
80
+ options?: { maxAttempts?: number },
81
+ ): Promise<T> {
82
+ const globalMax = options?.maxAttempts ?? 5;
83
+ let attempt = 0;
84
+
85
+ while (attempt < globalMax) {
86
+ try {
87
+ const result = await fn();
88
+ this.successCount++;
89
+ return result;
90
+ } catch (err: unknown) {
91
+ const error = err instanceof Error ? err : new Error(String(err));
92
+ const context: ErrorContext = {
93
+ sessionId,
94
+ error,
95
+ attempt,
96
+ timestamp: Date.now(),
97
+ };
98
+
99
+ const action = this.handleError(context);
100
+
101
+ // Non-recoverable actions — rethrow immediately
102
+ if (action.type === "abort" || action.type === "escalate" || action.type === "skip") {
103
+ this.failureCount++;
104
+ throw error;
105
+ }
106
+
107
+ // Enforce category-specific maxAttempts
108
+ if (action.maxAttempts !== undefined) {
109
+ const category = this.findCategory(action.reason);
110
+ const key = `${sessionId}::${category}`;
111
+ const tracker = this.attemptTracker.get(key) ?? { count: 0 };
112
+ tracker.count++;
113
+ this.attemptTracker.set(key, tracker);
114
+
115
+ if (tracker.count >= action.maxAttempts) {
116
+ this.failureCount++;
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ // Apply delay if specified
122
+ if (action.delay && action.delay > 0) {
123
+ await this.sleep(action.delay);
124
+ }
125
+
126
+ attempt++;
127
+ }
128
+ }
129
+
130
+ this.failureCount++;
131
+ // Exhausted global maxAttempts
132
+ throw new Error(
133
+ `[RecoveryHandler] Exhausted ${globalMax} attempts for session "${sessionId}"`,
134
+ );
135
+ }
136
+
137
+ /** Convenience: sleep for ms. */
138
+ private sleep(ms: number): Promise<void> {
139
+ return new Promise((resolve) => setTimeout(resolve, ms));
140
+ }
141
+
142
+ /**
143
+ * Derive a category from an action's reason string.
144
+ * Falls back to "timeout" if no pattern matches.
145
+ */
146
+ private findCategory(reason: string): ErrorCategory {
147
+ for (const entry of PATTERNS) {
148
+ if (entry.pattern.test(reason)) {
149
+ return entry.category;
150
+ }
151
+ }
152
+ // Attempt to extract from the reason string heuristically
153
+ const lower = reason.toLowerCase();
154
+ if (lower.includes("rate")) return "rate_limit";
155
+ if (lower.includes("context") || lower.includes("token")) return "context_overflow";
156
+ if (lower.includes("network") || lower.includes("econnrefused")) return "network";
157
+ if (lower.includes("session")) return "session";
158
+ if (lower.includes("tool")) return "tool_error";
159
+ if (lower.includes("parse") || lower.includes("json")) return "parse_error";
160
+ if (lower.includes("gibberish")) return "gibberish";
161
+ if (lower.includes("lsp") || lower.includes("tsc") || lower.includes("eslint")) {
162
+ return "lsp_diagnostic";
163
+ }
164
+ if (lower.includes("timeout") || lower.includes("timed")) return "timeout";
165
+ return "timeout";
166
+ }
167
+
168
+ /** Accumulated statistics. */
169
+ getStats(): RecoveryStats {
170
+ const byCategory: Record<ErrorCategory, number> = {
171
+ rate_limit: 0,
172
+ context_overflow: 0,
173
+ network: 0,
174
+ session: 0,
175
+ tool_error: 0,
176
+ parse_error: 0,
177
+ gibberish: 0,
178
+ lsp_diagnostic: 0,
179
+ timeout: 0,
180
+ };
181
+
182
+ const byAction: Record<RecoveryActionType, number> = {
183
+ retry: 0,
184
+ abort: 0,
185
+ skip: 0,
186
+ escalate: 0,
187
+ compact: 0,
188
+ };
189
+
190
+ for (const record of this.history) {
191
+ const cat = this.findCategory(record.action.reason);
192
+ if (byCategory[cat] !== undefined) byCategory[cat]++;
193
+ if (byAction[record.action.type] !== undefined) byAction[record.action.type]++;
194
+ }
195
+
196
+ const totalRecoveries = this.history.length;
197
+ const totalAttempts = this.successCount + this.failureCount;
198
+ const successRate = totalAttempts > 0 ? this.successCount / totalAttempts : 0;
199
+
200
+ return { totalRecoveries, byCategory, byAction, successRate };
201
+ }
202
+
203
+ /** Recent recovery records, most recent first. */
204
+ getHistory(limit?: number): RecoveryRecord[] {
205
+ const sorted = [...this.history].sort((a, b) => b.timestamp - a.timestamp);
206
+ return limit ? sorted.slice(0, limit) : sorted;
207
+ }
208
+
209
+ /** Clear all records and attempt state for a given session. */
210
+ clearSession(sessionId: string): void {
211
+ this.history = this.history.filter((r) => r.context.sessionId !== sessionId);
212
+ for (const key of this.attemptTracker.keys()) {
213
+ if (key.startsWith(`${sessionId}::`)) {
214
+ this.attemptTracker.delete(key);
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Reset all state (useful in tests). */
220
+ reset(): void {
221
+ this.history = [];
222
+ this.attemptTracker.clear();
223
+ this.successCount = 0;
224
+ this.failureCount = 0;
225
+ }
226
+
227
+ // ── private helpers ──
228
+
229
+ private record(context: ErrorContext, action: RecoveryAction): void {
230
+ // Update attempt tracker using the derived category from the action's reason
231
+ const category = this.findCategory(action.reason);
232
+ const key = `${context.sessionId}::${category}`;
233
+ if (!this.attemptTracker.has(key)) {
234
+ this.attemptTracker.set(key, { count: 0 });
235
+ }
236
+
237
+ this.history.push({
238
+ context,
239
+ action,
240
+ timestamp: Date.now(),
241
+ });
242
+ }
243
+ }
@@ -0,0 +1,14 @@
1
+ // Auto-Recovery module — barrel export.
2
+
3
+ export type {
4
+ ErrorCategory,
5
+ RecoveryActionType,
6
+ RecoveryAction,
7
+ ErrorContext,
8
+ RecoveryRecord,
9
+ RecoveryStats,
10
+ } from "./interfaces.ts";
11
+
12
+ export { RecoveryHandler } from "./handler.ts";
13
+ export { PATTERNS, escalateAction } from "./patterns.ts";
14
+ export type { ErrorPattern } from "./patterns.ts";
@@ -0,0 +1,48 @@
1
+ // Auto-Recovery type definitions for sub-agent error handling.
2
+
3
+ export type ErrorCategory =
4
+ | "rate_limit"
5
+ | "context_overflow"
6
+ | "network"
7
+ | "session"
8
+ | "tool_error"
9
+ | "parse_error"
10
+ | "gibberish"
11
+ | "lsp_diagnostic"
12
+ | "timeout";
13
+
14
+ export type RecoveryActionType =
15
+ | "retry"
16
+ | "abort"
17
+ | "skip"
18
+ | "escalate"
19
+ | "compact";
20
+
21
+ export interface RecoveryAction {
22
+ type: RecoveryActionType;
23
+ delay?: number; // ms delay before retry
24
+ maxAttempts?: number; // max retry attempts
25
+ reason: string;
26
+ modifyPrompt?: string; // instruction to prepend to retry prompt
27
+ }
28
+
29
+ export interface ErrorContext {
30
+ sessionId: string;
31
+ error: Error;
32
+ attempt: number;
33
+ timestamp: number;
34
+ agent?: string;
35
+ }
36
+
37
+ export interface RecoveryRecord {
38
+ context: ErrorContext;
39
+ action: RecoveryAction;
40
+ timestamp: number;
41
+ }
42
+
43
+ export interface RecoveryStats {
44
+ totalRecoveries: number;
45
+ byCategory: Record<ErrorCategory, number>;
46
+ byAction: Record<RecoveryActionType, number>;
47
+ successRate: number;
48
+ }
@@ -0,0 +1,149 @@
1
+ // Categorized error patterns with recovery action generators.
2
+ // Each pattern is a RegExp matched against error messages.
3
+ // The first match wins. If none match, the handler escalates.
4
+
5
+ import type { ErrorCategory, ErrorContext, RecoveryAction } from "./interfaces.ts";
6
+
7
+ export interface ErrorPattern {
8
+ pattern: RegExp;
9
+ category: ErrorCategory;
10
+ getAction: (ctx: ErrorContext) => RecoveryAction;
11
+ }
12
+
13
+ /**
14
+ * Default backoff delays (ms) for retries: [initial, second, third]
15
+ */
16
+ const RATE_LIMIT_BACKOFF = [1_000, 2_000, 4_000] as const;
17
+ const NETWORK_BACKOFF = [500, 1_000, 2_000] as const;
18
+
19
+ export const PATTERNS: ErrorPattern[] = [
20
+ // ── rate_limit ─────────────────────────────────────────────────────
21
+ {
22
+ pattern: /rate.?limit|too.?many.?requests|429/i,
23
+ category: "rate_limit",
24
+ getAction: (ctx: ErrorContext): RecoveryAction => {
25
+ const delay = RATE_LIMIT_BACKOFF[Math.min(ctx.attempt, RATE_LIMIT_BACKOFF.length - 1)];
26
+ return {
27
+ type: "retry",
28
+ delay,
29
+ maxAttempts: 3,
30
+ reason: `Rate limited (attempt ${ctx.attempt + 1}): backing off ${delay}ms`,
31
+ modifyPrompt: undefined,
32
+ };
33
+ },
34
+ },
35
+
36
+ // ── context_overflow ──────────────────────────────────────────────
37
+ {
38
+ pattern: /context.?length|token.?limit|maximum.?context/i,
39
+ category: "context_overflow",
40
+ getAction: (): RecoveryAction => ({
41
+ type: "compact",
42
+ maxAttempts: 2,
43
+ reason: "Context overflow detected: compacting before retry",
44
+ modifyPrompt:
45
+ "--- COMPACTED: Previous context exceeded token limits. Continue from here, preserving only essential state. ---",
46
+ }),
47
+ },
48
+
49
+ // ── network ───────────────────────────────────────────────────────
50
+ {
51
+ pattern: /ECONNREFUSED|ETIMEDOUT|network|fetch.?failed/i,
52
+ category: "network",
53
+ getAction: (ctx: ErrorContext): RecoveryAction => {
54
+ const delay = NETWORK_BACKOFF[Math.min(ctx.attempt, NETWORK_BACKOFF.length - 1)];
55
+ return {
56
+ type: "retry",
57
+ delay,
58
+ maxAttempts: 3,
59
+ reason: `Network error (attempt ${ctx.attempt + 1}): retrying in ${delay}ms`,
60
+ };
61
+ },
62
+ },
63
+
64
+ // ── session ───────────────────────────────────────────────────────
65
+ {
66
+ pattern: /session.?not.?found|session.?expired/i,
67
+ category: "session",
68
+ getAction: (): RecoveryAction => ({
69
+ type: "abort",
70
+ reason: "Session not found or expired — cannot recover",
71
+ }),
72
+ },
73
+
74
+ // ── tool_error ────────────────────────────────────────────────────
75
+ {
76
+ pattern: /tool.?not.?found|unknown.?tool/i,
77
+ category: "tool_error",
78
+ getAction: (): RecoveryAction => ({
79
+ type: "escalate",
80
+ reason: "Unknown or missing tool — orchestrator must decide",
81
+ }),
82
+ },
83
+
84
+ // ── parse_error ───────────────────────────────────────────────────
85
+ {
86
+ pattern: /parse.?error|invalid.?json|syntax.?error/i,
87
+ category: "parse_error",
88
+ getAction: (ctx: ErrorContext): RecoveryAction => ({
89
+ type: "retry",
90
+ delay: 0,
91
+ maxAttempts: 2,
92
+ reason: `Parse error on attempt ${ctx.attempt + 1}: retrying without modification`,
93
+ }),
94
+ },
95
+
96
+ // ── gibberish ─────────────────────────────────────────────────────
97
+ // Broad heuristic on output quality — catches gibberish, keyboard mash,
98
+ // repeated single characters, and long non-alphabetic sequences.
99
+ {
100
+ pattern: /gibberish|nonsens|unintelligible|asdfgh|qwerty|xxxxx|sdfsdf|[^a-z\s]{10,}|(.)\1{4,}/i,
101
+ category: "gibberish",
102
+ getAction: (): RecoveryAction => ({
103
+ type: "retry",
104
+ delay: 0,
105
+ maxAttempts: 2,
106
+ reason: "Gibberish output detected — retrying with clean context",
107
+ modifyPrompt:
108
+ "--- RECOVERY: Previous response was incoherent. Restart with only essential instructions. ---",
109
+ }),
110
+ },
111
+
112
+ // ── lsp_diagnostic ────────────────────────────────────────────────
113
+ {
114
+ pattern: /lsp.?diagnostic|tsc.?error|eslint.?error/i,
115
+ category: "lsp_diagnostic",
116
+ getAction: (): RecoveryAction => ({
117
+ type: "retry",
118
+ delay: 0,
119
+ maxAttempts: 2,
120
+ reason: "LSP diagnostic error — retrying with diagnostics in context",
121
+ modifyPrompt:
122
+ "--- RECOVERY: The following LSP diagnostics were detected in the last attempt. Fix them before continuing. ---",
123
+ }),
124
+ },
125
+
126
+ // ── timeout ───────────────────────────────────────────────────────
127
+ {
128
+ pattern: /execution.?timed.?out|timeout/i,
129
+ category: "timeout",
130
+ getAction: (): RecoveryAction => ({
131
+ type: "retry",
132
+ delay: 0,
133
+ maxAttempts: 2,
134
+ reason: "Execution timed out — retrying with task breakdown hint",
135
+ modifyPrompt:
136
+ "--- RECOVERY: Previous execution timed out. Break the task into smaller steps. ---",
137
+ }),
138
+ },
139
+ ];
140
+
141
+ /**
142
+ * Default escalation action when no pattern matches.
143
+ */
144
+ export function escalateAction(ctx: ErrorContext): RecoveryAction {
145
+ return {
146
+ type: "escalate",
147
+ reason: `Unrecognized error "${ctx.error.message}" — escalating to orchestrator`,
148
+ };
149
+ }