@automagik/genie 4.260409.12 → 4.260409.13
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/genie.js +114 -114
- package/package.json +1 -1
- package/plugins/genie/.claude-plugin/plugin.json +1 -1
- package/plugins/genie/package.json +1 -1
- package/src/lib/provider-adapters.ts +15 -8
- package/src/services/executors/claude-code.ts +2 -1
- package/src/services/omni-bridge.ts +34 -86
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genie",
|
|
3
|
-
"version": "4.260409.
|
|
3
|
+
"version": "4.260409.13",
|
|
4
4
|
"description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Namastex Labs"
|
|
@@ -97,6 +97,8 @@ export interface SpawnParams {
|
|
|
97
97
|
newWindow?: boolean;
|
|
98
98
|
/** Tmux window target to split into (e.g., "genie:3"). */
|
|
99
99
|
windowTarget?: string;
|
|
100
|
+
/** Skip genie hook dispatch injection (e.g., omni-originated sessions that shouldn't trigger orchestration hooks). */
|
|
101
|
+
skipHooks?: boolean;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/** Result of a successful launch-command build. */
|
|
@@ -353,17 +355,20 @@ export function buildClaudeCommand(params: SpawnParams): LaunchCommand {
|
|
|
353
355
|
|
|
354
356
|
appendSystemPromptFlags(parts, params);
|
|
355
357
|
|
|
356
|
-
// Inject hook dispatch + permissions via --settings (deep-merges with existing settings)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const settingsObj: Record<string, unknown> = {
|
|
360
|
-
|
|
358
|
+
// Inject hook dispatch + permissions via --settings (deep-merges with existing settings).
|
|
359
|
+
// Skip hooks for omni-originated sessions to prevent orchestration side-effects
|
|
360
|
+
// (e.g., auto-spawning qa/configure agents from seller sessions).
|
|
361
|
+
const settingsObj: Record<string, unknown> = {};
|
|
362
|
+
if (!params.skipHooks) {
|
|
363
|
+
const dispatchCmd = buildDispatchCommand();
|
|
364
|
+
const hookEntry = { type: 'command', command: dispatchCmd, timeout: 15 };
|
|
365
|
+
settingsObj.hooks = {
|
|
361
366
|
PreToolUse: [{ matcher: '*', hooks: [hookEntry] }],
|
|
362
367
|
PostToolUse: [{ matcher: '*', hooks: [hookEntry] }],
|
|
363
368
|
UserPromptSubmit: [{ hooks: [hookEntry] }],
|
|
364
369
|
Stop: [{ hooks: [hookEntry] }],
|
|
365
|
-
}
|
|
366
|
-
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
367
372
|
// Merge permissions into settings when provided
|
|
368
373
|
if (params.permissions) {
|
|
369
374
|
const perms: Record<string, string[]> = {};
|
|
@@ -371,7 +376,9 @@ export function buildClaudeCommand(params: SpawnParams): LaunchCommand {
|
|
|
371
376
|
if (params.permissions.deny?.length) perms.deny = params.permissions.deny;
|
|
372
377
|
if (Object.keys(perms).length > 0) settingsObj.permissions = perms;
|
|
373
378
|
}
|
|
374
|
-
|
|
379
|
+
if (Object.keys(settingsObj).length > 0) {
|
|
380
|
+
parts.push('--settings', escapeShellArg(JSON.stringify(settingsObj)));
|
|
381
|
+
}
|
|
375
382
|
|
|
376
383
|
// Emit --disallowedTools when the agent declares tool restrictions
|
|
377
384
|
if (params.disallowedTools?.length) {
|
|
@@ -129,6 +129,7 @@ export function buildOmniSpawnParams(
|
|
|
129
129
|
promptMode: entry.promptMode,
|
|
130
130
|
systemPromptFile: join(entry.dir, 'AGENTS.md'),
|
|
131
131
|
initialPrompt: fullInitialPrompt,
|
|
132
|
+
skipHooks: true,
|
|
132
133
|
nativeTeam: {
|
|
133
134
|
enabled: true,
|
|
134
135
|
agentName,
|
|
@@ -226,7 +227,7 @@ export class ClaudeCodeOmniExecutor implements IExecutor {
|
|
|
226
227
|
if (!this.safePgCall) return null;
|
|
227
228
|
const agent = await this.safePgCall(
|
|
228
229
|
'tmux-find-or-create-agent',
|
|
229
|
-
() => agents.findOrCreateAgent(agentName
|
|
230
|
+
() => agents.findOrCreateAgent(`${agentName}:${chatId}`, 'omni', 'omni'),
|
|
230
231
|
null,
|
|
231
232
|
{ chatId },
|
|
232
233
|
);
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { type NatsConnection, StringCodec, type Subscription, connect } from 'nats';
|
|
14
14
|
import type { Sql } from '../lib/db.js';
|
|
15
15
|
import { resolveExecutorType } from '../lib/executor-config.js';
|
|
16
|
-
import { isPaneAlive } from '../lib/tmux.js';
|
|
17
16
|
import { BridgeSessionStore } from './bridge-session-store.js';
|
|
18
17
|
import type { ExecutorSession, IExecutor, OmniMessage } from './executor.js';
|
|
19
18
|
import { ClaudeCodeOmniExecutor } from './executors/claude-code.js';
|
|
@@ -188,6 +187,8 @@ export class OmniBridge {
|
|
|
188
187
|
*/
|
|
189
188
|
private sessions = new Map<string, SessionEntry>();
|
|
190
189
|
private messageQueue: OmniMessage[] = [];
|
|
190
|
+
/** Dedup cache: messageId → receive timestamp. Prevents JetStream redelivery duplicates. */
|
|
191
|
+
private recentMessageIds = new Map<string, number>();
|
|
191
192
|
private idleCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
192
193
|
private sc = StringCodec();
|
|
193
194
|
|
|
@@ -288,13 +289,13 @@ export class OmniBridge {
|
|
|
288
289
|
});
|
|
289
290
|
|
|
290
291
|
// Subscribe to all omni messages
|
|
291
|
-
this.sub = this.nc.subscribe('omni.message.>');
|
|
292
|
+
this.sub = this.nc.subscribe('omni.message.>', { queue: 'genie-bridge' });
|
|
292
293
|
this.processSubscription();
|
|
293
294
|
|
|
294
295
|
// Turn lifecycle events from Omni
|
|
295
296
|
const turnSubs = ['omni.turn.open.>', 'omni.turn.done.>', 'omni.turn.nudge.>', 'omni.turn.timeout.>'];
|
|
296
297
|
for (const topic of turnSubs) {
|
|
297
|
-
const sub = this.nc.subscribe(topic);
|
|
298
|
+
const sub = this.nc.subscribe(topic, { queue: 'genie-bridge' });
|
|
298
299
|
this.processTurnEvents(sub);
|
|
299
300
|
}
|
|
300
301
|
|
|
@@ -302,7 +303,7 @@ export class OmniBridge {
|
|
|
302
303
|
// sends a reset token (e.g. 🗑️). Paired with omni-side publisher in
|
|
303
304
|
// automagik-dev/omni#361. Subject hierarchy: omni.session.reset.{instance}.{chat}
|
|
304
305
|
// (recursive `>` because WhatsApp chat ids contain dots).
|
|
305
|
-
const sessionResetSub = this.nc.subscribe('omni.session.reset.>');
|
|
306
|
+
const sessionResetSub = this.nc.subscribe('omni.session.reset.>', { queue: 'genie-bridge' });
|
|
306
307
|
this.processSessionResetEvents(sessionResetSub);
|
|
307
308
|
|
|
308
309
|
// Start idle session checker
|
|
@@ -500,94 +501,23 @@ export class OmniBridge {
|
|
|
500
501
|
}
|
|
501
502
|
|
|
502
503
|
/**
|
|
503
|
-
*
|
|
504
|
+
* Clean up stale sessions from previous bridge runs.
|
|
504
505
|
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* orphaned. This runs once during start(), after sessionStore is initialized.
|
|
506
|
+
* Orphans ALL active sessions on startup. Live panes will be re-created
|
|
507
|
+
* on demand when the next message arrives. This prevents duplicate key
|
|
508
|
+
* errors and misrouting from stale session state.
|
|
509
509
|
*/
|
|
510
510
|
private async recoverSessions(): Promise<void> {
|
|
511
511
|
if (!this.sessionStore) return;
|
|
512
512
|
|
|
513
|
-
const
|
|
514
|
-
'
|
|
515
|
-
(sql) => new BridgeSessionStore(sql).
|
|
516
|
-
|
|
513
|
+
const orphanedCount = await this.safePgCall(
|
|
514
|
+
'recover_orphan_all',
|
|
515
|
+
(sql) => new BridgeSessionStore(sql).markAllOrphaned(),
|
|
516
|
+
0,
|
|
517
517
|
);
|
|
518
518
|
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
console.log(`[omni-bridge] Found ${activeSessions.length} active session(s) from previous run, recovering...`);
|
|
522
|
-
|
|
523
|
-
const orphanIds: string[] = [];
|
|
524
|
-
|
|
525
|
-
for (const row of activeSessions) {
|
|
526
|
-
const key = `${row.agent_name}:${row.chat_id}`;
|
|
527
|
-
|
|
528
|
-
// SDK sessions can't survive a process restart — always orphan them.
|
|
529
|
-
if (!row.tmux_pane_id) {
|
|
530
|
-
orphanIds.push(row.id);
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Duplicate guard: if another row already recovered this chat, orphan the older one.
|
|
535
|
-
if (this.sessions.has(key)) {
|
|
536
|
-
orphanIds.push(row.id);
|
|
537
|
-
continue;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Check if the tmux pane is still alive.
|
|
541
|
-
let alive = false;
|
|
542
|
-
try {
|
|
543
|
-
alive = await isPaneAlive(row.tmux_pane_id);
|
|
544
|
-
} catch {
|
|
545
|
-
// tmux not available or pane check failed — treat as dead.
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (!alive) {
|
|
549
|
-
orphanIds.push(row.id);
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Pane is alive — reconstruct the in-memory session entry.
|
|
554
|
-
const session: ExecutorSession = {
|
|
555
|
-
id: row.executor_id ?? row.id,
|
|
556
|
-
agentName: row.agent_name,
|
|
557
|
-
chatId: row.chat_id,
|
|
558
|
-
executorType: 'tmux',
|
|
559
|
-
createdAt: new Date(row.started_at).getTime(),
|
|
560
|
-
lastActivityAt: new Date(row.last_activity_at).getTime(),
|
|
561
|
-
tmux: { session: '', window: '', paneId: row.tmux_pane_id },
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
const entry: SessionEntry = {
|
|
565
|
-
session,
|
|
566
|
-
instanceId: row.instance_id,
|
|
567
|
-
spawning: false,
|
|
568
|
-
buffer: [],
|
|
569
|
-
idleTimer: null,
|
|
570
|
-
pgBridgeSessionId: row.id,
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
this.sessions.set(key, entry);
|
|
574
|
-
this.resetIdleTimer(key);
|
|
575
|
-
|
|
576
|
-
console.log(`[omni-bridge] Recovered session ${key} (pane=${row.tmux_pane_id})`);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Mark dead/SDK sessions as orphaned in one batch.
|
|
580
|
-
if (orphanIds.length > 0) {
|
|
581
|
-
await this.safePgCall(
|
|
582
|
-
'recover_mark_orphaned',
|
|
583
|
-
(sql) => new BridgeSessionStore(sql).markOrphaned(orphanIds),
|
|
584
|
-
undefined,
|
|
585
|
-
);
|
|
586
|
-
console.log(`[omni-bridge] Marked ${orphanIds.length} stale session(s) as orphaned`);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
if (this.sessions.size > 0) {
|
|
590
|
-
console.log(`[omni-bridge] Recovered ${this.sessions.size} live session(s)`);
|
|
519
|
+
if (orphanedCount > 0) {
|
|
520
|
+
console.log(`[omni-bridge] Startup cleanup: orphaned ${orphanedCount} stale session(s) from previous run`);
|
|
591
521
|
}
|
|
592
522
|
}
|
|
593
523
|
|
|
@@ -658,6 +588,24 @@ export class OmniBridge {
|
|
|
658
588
|
|
|
659
589
|
console.log(`[omni-bridge] NATS message received: ${msg.subject} agent=${parsed.agent} chat=${parsed.chatId}`);
|
|
660
590
|
|
|
591
|
+
// Dedup: skip messages already processed (JetStream redelivery protection)
|
|
592
|
+
// biome-ignore lint/suspicious/noExplicitAny: NATS payload has extra fields beyond OmniMessage
|
|
593
|
+
const messageId = (parsed as any).messageId as string | undefined;
|
|
594
|
+
if (messageId && this.recentMessageIds.has(messageId)) {
|
|
595
|
+
console.log(`[omni-bridge] Dedup: skipping duplicate messageId=${messageId}`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (messageId) {
|
|
599
|
+
this.recentMessageIds.set(messageId, Date.now());
|
|
600
|
+
// Prune stale entries when cache grows large (TTL 60s)
|
|
601
|
+
if (this.recentMessageIds.size > 1000) {
|
|
602
|
+
const cutoff = Date.now() - 60_000;
|
|
603
|
+
for (const [id, ts] of this.recentMessageIds) {
|
|
604
|
+
if (ts < cutoff) this.recentMessageIds.delete(id);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
661
609
|
if (!parsed.chatId || !parsed.agent) {
|
|
662
610
|
console.warn('[omni-bridge] Dropping message: missing chatId or agent', msg.subject);
|
|
663
611
|
continue;
|
|
@@ -999,7 +947,7 @@ export class OmniBridge {
|
|
|
999
947
|
OMNI_INSTANCE: payloadEnv?.OMNI_INSTANCE ?? message.instanceId,
|
|
1000
948
|
OMNI_CHAT: payloadEnv?.OMNI_CHAT ?? message.chatId,
|
|
1001
949
|
OMNI_MESSAGE: payloadEnv?.OMNI_MESSAGE ?? (raw.messageId as string) ?? '',
|
|
1002
|
-
OMNI_TURN_ID: payloadEnv?.OMNI_TURN_ID
|
|
950
|
+
OMNI_TURN_ID: payloadEnv?.OMNI_TURN_ID || '',
|
|
1003
951
|
OMNI_SENDER_NAME: payloadEnv?.OMNI_SENDER_NAME ?? message.sender ?? '',
|
|
1004
952
|
};
|
|
1005
953
|
|