@happycastle/oh-my-openclaw 0.9.1 → 0.11.0

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.
@@ -0,0 +1,30 @@
1
+ export type ContextPriority = 'critical' | 'high' | 'normal' | 'low';
2
+ export type ContextSourceType = 'persona' | 'todo-enforcer' | 'system' | 'plugin';
3
+ export interface ContextEntry {
4
+ id: string;
5
+ content: string;
6
+ priority: ContextPriority;
7
+ source: ContextSourceType;
8
+ oneShot?: boolean;
9
+ }
10
+ export interface RegisterContextOptions {
11
+ id: string;
12
+ content: string;
13
+ priority?: ContextPriority;
14
+ source?: ContextSourceType;
15
+ oneShot?: boolean;
16
+ }
17
+ export declare class ContextCollector {
18
+ private sessions;
19
+ register(sessionKey: string, options: RegisterContextOptions): void;
20
+ unregister(sessionKey: string, entryId: string): void;
21
+ collect(sessionKey: string): ContextEntry[];
22
+ collectAsString(sessionKey: string, separator?: string): string;
23
+ clear(sessionKey: string): void;
24
+ clearAll(): void;
25
+ getEntries(sessionKey: string): ContextEntry[];
26
+ hasEntries(sessionKey: string): boolean;
27
+ private getOrCreateSession;
28
+ private sortEntries;
29
+ }
30
+ export declare const contextCollector: ContextCollector;
@@ -0,0 +1,85 @@
1
+ const PRIORITY_ORDER = {
2
+ critical: 0,
3
+ high: 1,
4
+ normal: 2,
5
+ low: 3,
6
+ };
7
+ export class ContextCollector {
8
+ sessions = new Map();
9
+ register(sessionKey, options) {
10
+ const sessionEntries = this.getOrCreateSession(sessionKey);
11
+ const entry = {
12
+ id: options.id,
13
+ content: options.content,
14
+ priority: options.priority ?? 'normal',
15
+ source: options.source ?? 'plugin',
16
+ oneShot: options.oneShot ?? false,
17
+ };
18
+ sessionEntries.set(options.id, entry);
19
+ }
20
+ unregister(sessionKey, entryId) {
21
+ const sessionEntries = this.sessions.get(sessionKey);
22
+ if (!sessionEntries) {
23
+ return;
24
+ }
25
+ sessionEntries.delete(entryId);
26
+ if (sessionEntries.size === 0) {
27
+ this.sessions.delete(sessionKey);
28
+ }
29
+ }
30
+ collect(sessionKey) {
31
+ const sessionEntries = this.sessions.get(sessionKey);
32
+ if (!sessionEntries) {
33
+ return [];
34
+ }
35
+ const entries = this.sortEntries([...sessionEntries.values()]);
36
+ for (const entry of entries) {
37
+ if (entry.oneShot) {
38
+ sessionEntries.delete(entry.id);
39
+ }
40
+ }
41
+ if (sessionEntries.size === 0) {
42
+ this.sessions.delete(sessionKey);
43
+ }
44
+ return entries;
45
+ }
46
+ collectAsString(sessionKey, separator = '\n\n') {
47
+ const entries = this.collect(sessionKey);
48
+ return entries.map((entry) => entry.content).join(separator);
49
+ }
50
+ clear(sessionKey) {
51
+ this.sessions.delete(sessionKey);
52
+ }
53
+ clearAll() {
54
+ this.sessions.clear();
55
+ }
56
+ getEntries(sessionKey) {
57
+ const sessionEntries = this.sessions.get(sessionKey);
58
+ if (!sessionEntries) {
59
+ return [];
60
+ }
61
+ return this.sortEntries([...sessionEntries.values()]);
62
+ }
63
+ hasEntries(sessionKey) {
64
+ const sessionEntries = this.sessions.get(sessionKey);
65
+ return Boolean(sessionEntries && sessionEntries.size > 0);
66
+ }
67
+ getOrCreateSession(sessionKey) {
68
+ let sessionEntries = this.sessions.get(sessionKey);
69
+ if (!sessionEntries) {
70
+ sessionEntries = new Map();
71
+ this.sessions.set(sessionKey, sessionEntries);
72
+ }
73
+ return sessionEntries;
74
+ }
75
+ sortEntries(entries) {
76
+ return entries.sort((a, b) => {
77
+ const priorityDiff = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
78
+ if (priorityDiff !== 0) {
79
+ return priorityDiff;
80
+ }
81
+ return a.id.localeCompare(b.id);
82
+ });
83
+ }
84
+ }
85
+ export const contextCollector = new ContextCollector();
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ export declare function registerContextInjector(api: OmocPluginApi): void;
@@ -0,0 +1,20 @@
1
+ import { contextCollector } from '../features/context-collector.js';
2
+ export function registerContextInjector(api) {
3
+ api.registerHook('before_prompt_build', (event) => {
4
+ const sessionKey = event.agentId || 'default';
5
+ if (!contextCollector.hasEntries(sessionKey)) {
6
+ return event;
7
+ }
8
+ const entryCount = contextCollector.getEntries(sessionKey).length;
9
+ const collectedContext = contextCollector.collectAsString(sessionKey);
10
+ if (!collectedContext) {
11
+ return event;
12
+ }
13
+ event.prependContext = collectedContext;
14
+ api.logger.info(`[omoc] Context injected: ${entryCount} entries for ${sessionKey}`);
15
+ return event;
16
+ }, {
17
+ name: 'oh-my-openclaw.context-injector',
18
+ description: 'Unified context injection from ContextCollector into prependContext',
19
+ });
20
+ }
@@ -1,6 +1,7 @@
1
1
  import { OmocPluginApi } from '../types.js';
2
+ export declare function resetPersonaContextEntries(): void;
2
3
  export declare function resetPersonaInjectorState(): void;
3
4
  export declare function getPersonaInjectorState(): {
4
- lastPersonaInjectionTime: number;
5
+ lastInjectedPersonaId: string | null;
5
6
  };
6
7
  export declare function registerPersonaInjector(api: OmocPluginApi): void;
@@ -1,47 +1,56 @@
1
1
  import { getActivePersona } from '../utils/persona-state.js';
2
2
  import { readPersonaPromptSync } from '../agents/persona-prompts.js';
3
- /** Minimum interval (ms) between persona injections to prevent regurgitation. */
4
- const PERSONA_INJECTION_COOLDOWN_MS = 5_000;
5
- let lastPersonaInjectionTime = 0;
3
+ import { contextCollector } from '../features/context-collector.js';
4
+ let lastInjectedPersonaId = null;
5
+ const personaSessionKeys = new Set();
6
+ export function resetPersonaContextEntries() {
7
+ for (const sessionKey of personaSessionKeys) {
8
+ const entries = contextCollector.getEntries(sessionKey);
9
+ for (const entry of entries) {
10
+ if (entry.source === 'persona') {
11
+ contextCollector.unregister(sessionKey, entry.id);
12
+ }
13
+ }
14
+ }
15
+ personaSessionKeys.clear();
16
+ }
6
17
  export function resetPersonaInjectorState() {
7
- lastPersonaInjectionTime = 0;
18
+ lastInjectedPersonaId = null;
19
+ resetPersonaContextEntries();
8
20
  }
9
21
  export function getPersonaInjectorState() {
10
- return { lastPersonaInjectionTime };
22
+ return { lastInjectedPersonaId };
11
23
  }
12
24
  export function registerPersonaInjector(api) {
13
25
  api.registerHook('agent:bootstrap', (event) => {
14
26
  const personaId = getActivePersona();
27
+ const sessionKey = event.context.agentId || 'default';
15
28
  if (!personaId) {
16
29
  return;
17
30
  }
18
- if (!event.context.bootstrapFiles) {
19
- event.context.bootstrapFiles = [];
20
- }
21
- const alreadyInjected = event.context.bootstrapFiles.some((f) => f.path.startsWith('omoc://persona/'));
22
- if (alreadyInjected) {
23
- api.logger.info(`[omoc] Persona injection skipped (already present in bootstrapFiles)`);
24
- return;
25
- }
26
- const now = Date.now();
27
- if (now - lastPersonaInjectionTime < PERSONA_INJECTION_COOLDOWN_MS) {
28
- api.logger.info(`[omoc] Persona injection skipped (cooldown)`);
31
+ if (lastInjectedPersonaId === personaId) {
29
32
  return;
30
33
  }
31
34
  try {
35
+ if (lastInjectedPersonaId) {
36
+ contextCollector.unregister(sessionKey, `persona/${lastInjectedPersonaId}`);
37
+ }
32
38
  const content = readPersonaPromptSync(personaId);
33
- event.context.bootstrapFiles.push({
34
- path: `omoc://persona/${personaId}`,
39
+ contextCollector.register(sessionKey, {
40
+ id: `persona/${personaId}`,
35
41
  content,
42
+ priority: 'high',
43
+ source: 'persona',
36
44
  });
37
- lastPersonaInjectionTime = now;
38
- api.logger.info(`[omoc] Persona injected: ${personaId}`);
45
+ personaSessionKeys.add(sessionKey);
46
+ lastInjectedPersonaId = personaId;
47
+ api.logger.info(`[omoc] Persona context registered: ${personaId}`);
39
48
  }
40
49
  catch (err) {
41
- api.logger.error(`[omoc] Failed to inject persona ${personaId}:`, err);
50
+ api.logger.error(`[omoc] Failed to register persona context ${personaId}:`, err);
42
51
  }
43
52
  }, {
44
53
  name: 'oh-my-openclaw.persona-injector',
45
- description: 'Injects active persona prompt into agent bootstrap (with dedup + cooldown)',
54
+ description: 'Injects active persona prompt once per persona change',
46
55
  });
47
56
  }
@@ -1,8 +1,6 @@
1
1
  import { OmocPluginApi } from '../types.js';
2
+ export type AgentRole = 'orchestrator' | 'worker' | 'lightweight';
3
+ export declare function classifyAgentRole(agentId?: string): AgentRole;
2
4
  export declare function resetEnforcerState(): void;
3
- export declare function getEnforcerState(): {
4
- lastInjectionTime: number;
5
- consecutiveFailures: number;
6
- disabledByFailures: boolean;
7
- };
5
+ export declare function getEnforcerState(): {};
8
6
  export declare function registerTodoEnforcer(api: OmocPluginApi): void;
@@ -1,28 +1,47 @@
1
1
  import { getConfig } from '../utils/config.js';
2
- const DIRECTIVE_TEXT = `[SYSTEM DIRECTIVE: OH-MY-OPENCLAW - TODO CONTINUATION]
3
- You MUST continue working on incomplete todos.
4
- - Do NOT stop until all tasks are marked complete
5
- - Do NOT ask for permission to continue
2
+ import { contextCollector } from '../features/context-collector.js';
3
+ const ORCHESTRATOR_IDS = new Set([
4
+ 'omoc_prometheus',
5
+ 'omoc_atlas',
6
+ ]);
7
+ const WORKER_IDS = new Set([
8
+ 'omoc_sisyphus',
9
+ 'omoc_hephaestus',
10
+ 'omoc_frontend',
11
+ ]);
12
+ export function classifyAgentRole(agentId) {
13
+ if (!agentId)
14
+ return 'orchestrator';
15
+ if (ORCHESTRATOR_IDS.has(agentId))
16
+ return 'orchestrator';
17
+ if (WORKER_IDS.has(agentId))
18
+ return 'worker';
19
+ return 'lightweight';
20
+ }
21
+ const ORCHESTRATOR_DIRECTIVE = `[SYSTEM REMINDER - TODO CONTINUATION]
22
+ If you have incomplete todos, continue working on them.
6
23
  - Mark each task complete immediately when finished
7
24
  - If blocked, document the blocker and move to next task
25
+ - Do NOT restate prior messages — output only deltas and next concrete action
26
+ - If no actionable next step remains, declare tasks complete and stop
8
27
 
9
- [SUBAGENT COMPLETION RULE]
10
- When you receive a subagent completion notification ("✅ Subagent finished"):
11
- - This is an ACTION TRIGGER, not an FYI
12
- - IMMEDIATELY check the subagent's result
13
- - Verify against success criteria
14
- - Then proceed to the next task/phase
15
- - NEVER stop after receiving a completion notification`;
16
- let lastInjectionTime = 0;
17
- let consecutiveFailures = 0;
18
- let disabledByFailures = false;
28
+ When you receive a subagent completion notification:
29
+ - Check the subagent's result against success criteria
30
+ - Then proceed to the next task/phase`;
31
+ const WORKER_DIRECTIVE = `[SYSTEM REMINDER - TASK COMPLETION]
32
+ Complete your assigned task, return the result, then stop.
33
+ - Do NOT restate prior messages — output only new findings or changes
34
+ - If blocked, report the blocker and stop`;
35
+ const DIRECTIVES = {
36
+ orchestrator: ORCHESTRATOR_DIRECTIVE,
37
+ worker: WORKER_DIRECTIVE,
38
+ lightweight: null,
39
+ };
19
40
  export function resetEnforcerState() {
20
- lastInjectionTime = 0;
21
- consecutiveFailures = 0;
22
- disabledByFailures = false;
41
+ // no-op — kept for API compatibility (no global state to reset)
23
42
  }
24
43
  export function getEnforcerState() {
25
- return { lastInjectionTime, consecutiveFailures, disabledByFailures };
44
+ return {};
26
45
  }
27
46
  export function registerTodoEnforcer(api) {
28
47
  api.registerHook('agent:bootstrap', (event) => {
@@ -30,41 +49,26 @@ export function registerTodoEnforcer(api) {
30
49
  if (!config.todo_enforcer_enabled) {
31
50
  return;
32
51
  }
33
- if (disabledByFailures) {
34
- api.logger.warn('[omoc] Todo enforcer disabled due to consecutive failures');
35
- return;
36
- }
37
- const now = Date.now();
38
- if (config.todo_enforcer_cooldown_ms > 0 && (now - lastInjectionTime) < config.todo_enforcer_cooldown_ms) {
39
- api.logger.info('[omoc] Todo enforcer skipped (cooldown)');
40
- return;
41
- }
42
- if (!event.context.bootstrapFiles) {
43
- event.context.bootstrapFiles = [];
44
- }
45
- const alreadyInjected = event.context.bootstrapFiles.some((f) => f.path === 'omoc://todo-enforcer');
46
- if (alreadyInjected) {
47
- api.logger.info('[omoc] Todo enforcer skipped (already present in bootstrapFiles)');
52
+ const role = classifyAgentRole(event.context.agentId);
53
+ const directive = DIRECTIVES[role];
54
+ const sessionKey = event.context.agentId || 'default';
55
+ if (!directive) {
48
56
  return;
49
57
  }
50
58
  try {
51
- event.context.bootstrapFiles.push({
52
- path: 'omoc://todo-enforcer',
53
- content: DIRECTIVE_TEXT,
59
+ contextCollector.register(sessionKey, {
60
+ id: 'todo-enforcer',
61
+ content: directive,
62
+ priority: 'normal',
63
+ source: 'todo-enforcer',
54
64
  });
55
- lastInjectionTime = now;
56
- consecutiveFailures = 0;
57
- api.logger.info('[omoc] Todo enforcer directive injected');
65
+ api.logger.info(`[omoc] Todo enforcer context registered (role: ${role})`);
58
66
  }
59
- catch {
60
- consecutiveFailures++;
61
- if (config.todo_enforcer_max_failures > 0 && consecutiveFailures >= config.todo_enforcer_max_failures) {
62
- disabledByFailures = true;
63
- api.logger.error(`[omoc] Todo enforcer disabled after ${consecutiveFailures} consecutive failures`);
64
- }
67
+ catch (err) {
68
+ api.logger.error('[omoc] Todo enforcer context registration failed:', err);
65
69
  }
66
70
  }, {
67
71
  name: 'oh-my-openclaw.todo-enforcer',
68
- description: 'Injects TODO continuation directive into agent bootstrap',
72
+ description: 'Injects role-aware TODO directive into agent bootstrap',
69
73
  });
70
74
  }
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { registerRalphCommands } from './commands/ralph-commands.js';
14
14
  import { registerStatusCommands } from './commands/status-commands.js';
15
15
  import { registerPersonaCommands } from './commands/persona-commands.js';
16
16
  import { registerPersonaInjector } from './hooks/persona-injector.js';
17
+ import { registerContextInjector } from './hooks/context-injector.js';
17
18
  import { registerSetupCli } from './cli/setup.js';
18
19
  const registry = {
19
20
  hooks: [],
@@ -65,6 +66,14 @@ export default function register(api) {
65
66
  catch (err) {
66
67
  api.logger.error(`[${PLUGIN_ID}] Failed to register Persona Injector:`, err);
67
68
  }
69
+ try {
70
+ registerContextInjector(api);
71
+ registry.hooks.push('context-injector');
72
+ api.logger.info(`[${PLUGIN_ID}] Context injector hook registered (before_prompt_build)`);
73
+ }
74
+ catch (err) {
75
+ api.logger.error(`[${PLUGIN_ID}] Failed to register Context Injector:`, err);
76
+ }
68
77
  try {
69
78
  registerRalphLoop(api);
70
79
  registry.services.push('ralph-loop');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Oh-My-OpenClaw plugin — multi-agent orchestration, todo enforcer, ralph loop, and custom tools for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",