@tenex-chat/backend 0.9.5 → 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.
- package/README.md +5 -1
- package/dist/daemon-wrapper.cjs +47 -0
- package/dist/index.js +59268 -0
- package/dist/wrapper.js +171 -0
- package/package.json +19 -27
- package/src/agents/AgentRegistry.ts +9 -7
- package/src/agents/AgentStorage.ts +24 -1
- package/src/agents/agent-installer.ts +6 -0
- package/src/agents/agent-loader.ts +7 -2
- package/src/agents/constants.ts +10 -2
- package/src/agents/execution/AgentExecutor.ts +35 -6
- package/src/agents/execution/StreamCallbacks.ts +53 -13
- package/src/agents/execution/StreamExecutionHandler.ts +110 -16
- package/src/agents/execution/StreamSetup.ts +19 -9
- package/src/agents/execution/ToolEventHandlers.ts +112 -0
- package/src/agents/role-categories.ts +53 -0
- package/src/agents/types/runtime.ts +7 -0
- package/src/agents/types/storage.ts +7 -0
- package/src/commands/agent/import/openclaw-distiller.ts +63 -7
- package/src/commands/agent/import/openclaw-reader.ts +54 -0
- package/src/commands/agent/import/openclaw.ts +120 -29
- package/src/commands/agent/index.ts +83 -2
- package/src/commands/setup/display.ts +123 -0
- package/src/commands/setup/embed.ts +13 -13
- package/src/commands/setup/global-system-prompt.ts +15 -17
- package/src/commands/setup/image.ts +17 -20
- package/src/commands/setup/interactive.ts +37 -20
- package/src/commands/setup/llm.ts +12 -7
- package/src/commands/setup/onboarding.ts +1580 -248
- package/src/commands/setup/providers.ts +3 -3
- package/src/conversations/ConversationStore.ts +23 -2
- package/src/conversations/MessageBuilder.ts +51 -73
- package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
- package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
- package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
- package/src/conversations/services/ConversationSummarizer.ts +1 -2
- package/src/conversations/types.ts +11 -0
- package/src/daemon/Daemon.ts +78 -57
- package/src/daemon/ProjectRuntime.ts +6 -12
- package/src/daemon/SubscriptionManager.ts +13 -0
- package/src/daemon/index.ts +0 -1
- package/src/event-handler/index.ts +1 -0
- package/src/index.ts +20 -1
- package/src/llm/ChunkHandler.ts +1 -1
- package/src/llm/FinishHandler.ts +28 -4
- package/src/llm/LLMConfigEditor.ts +218 -106
- package/src/llm/index.ts +0 -4
- package/src/llm/meta/MetaModelResolver.ts +3 -18
- package/src/llm/middleware/message-sanitizer.ts +153 -0
- package/src/llm/providers/ollama-models.ts +0 -38
- package/src/llm/service.ts +50 -15
- package/src/llm/types.ts +0 -12
- package/src/llm/utils/ConfigurationManager.ts +88 -465
- package/src/llm/utils/ConfigurationTester.ts +42 -185
- package/src/llm/utils/ModelSelector.ts +156 -92
- package/src/llm/utils/ProviderConfigUI.ts +10 -141
- package/src/llm/utils/models-dev-cache.ts +102 -23
- package/src/llm/utils/provider-select-prompt.ts +284 -0
- package/src/llm/utils/provider-setup.ts +81 -34
- package/src/llm/utils/variant-list-prompt.ts +361 -0
- package/src/nostr/AgentEventDecoder.ts +1 -0
- package/src/nostr/AgentEventEncoder.ts +37 -0
- package/src/nostr/AgentProfilePublisher.ts +13 -0
- package/src/nostr/AgentPublisher.ts +26 -0
- package/src/nostr/kinds.ts +1 -0
- package/src/nostr/ndkClient.ts +4 -1
- package/src/nostr/types.ts +12 -0
- package/src/prompts/fragments/25-rag-instructions.ts +22 -21
- package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
- package/src/prompts/fragments/index.ts +2 -0
- package/src/prompts/utils/systemPromptBuilder.ts +18 -28
- package/src/services/AgentDefinitionMonitor.ts +8 -0
- package/src/services/ConfigService.ts +34 -0
- package/src/services/PubkeyService.ts +7 -1
- package/src/services/compression/CompressionService.ts +133 -74
- package/src/services/compression/compression-utils.ts +110 -19
- package/src/services/config/types.ts +0 -6
- package/src/services/dispatch/AgentDispatchService.ts +79 -0
- package/src/services/intervention/InterventionService.ts +78 -5
- package/src/services/nip46/Nip46SigningService.ts +30 -1
- package/src/services/projects/ProjectContext.ts +8 -6
- package/src/services/rag/RAGCollectionRegistry.ts +199 -0
- package/src/services/rag/RAGDatabaseService.ts +2 -7
- package/src/services/rag/RAGOperations.ts +25 -45
- package/src/services/rag/RAGService.ts +0 -31
- package/src/services/rag/RagSubscriptionService.ts +71 -122
- package/src/services/rag/rag-utils.ts +13 -0
- package/src/services/ral/RALRegistry.ts +25 -184
- package/src/services/reports/ReportEmbeddingService.ts +63 -113
- package/src/services/search/UnifiedSearchService.ts +115 -4
- package/src/services/search/index.ts +1 -0
- package/src/services/search/projectFilter.ts +20 -4
- package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
- package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
- package/src/services/search/providers/LessonSearchProvider.ts +1 -8
- package/src/services/search/providers/ReportSearchProvider.ts +1 -0
- package/src/services/search/types.ts +24 -3
- package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
- package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
- package/src/telemetry/setup.ts +2 -13
- package/src/tools/implementations/ask.ts +3 -3
- package/src/tools/implementations/conversation_get.ts +28 -268
- package/src/tools/implementations/fs_grep.ts +6 -6
- package/src/tools/implementations/fs_read.ts +2 -0
- package/src/tools/implementations/fs_write.ts +2 -0
- package/src/tools/implementations/learn.ts +38 -50
- package/src/tools/implementations/rag_add_documents.ts +6 -4
- package/src/tools/implementations/rag_create_collection.ts +37 -4
- package/src/tools/implementations/rag_delete_collection.ts +9 -0
- package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
- package/src/tools/registry.ts +7 -8
- package/src/tools/types.ts +11 -2
- package/src/tools/utils/transcript-args.ts +13 -0
- package/src/utils/cli-theme.ts +13 -0
- package/src/utils/logger.ts +55 -0
- package/src/utils/metadataKeys.ts +17 -0
- package/src/utils/sqlEscaping.ts +39 -0
- package/src/wrapper.ts +7 -3
- package/dist/src/index.js +0 -46790
- package/dist/tenex-backend-wrapper.cjs +0 -3
- package/src/agents/execution/constants.ts +0 -16
- package/src/agents/execution/index.ts +0 -3
- package/src/agents/index.ts +0 -4
- package/src/commands/agent.ts +0 -235
- package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
- package/src/conversations/formatters/index.ts +0 -9
- package/src/conversations/index.ts +0 -2
- package/src/conversations/utils/content-utils.ts +0 -69
- package/src/daemon/UnixSocketTransport.ts +0 -318
- package/src/event-handler/newConversation.ts +0 -165
- package/src/events/NDKProjectStatus.ts +0 -384
- package/src/events/index.ts +0 -4
- package/src/lib/json-parser.ts +0 -30
- package/src/llm/RecordingState.ts +0 -37
- package/src/llm/StreamPublisher.ts +0 -40
- package/src/llm/middleware/flight-recorder.ts +0 -188
- package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
- package/src/nostr/constants.ts +0 -38
- package/src/prompts/core/index.ts +0 -3
- package/src/prompts/index.ts +0 -21
- package/src/services/image/index.ts +0 -12
- package/src/services/status/index.ts +0 -11
- package/src/telemetry/diagnostics.ts +0 -27
- package/src/tools/implementations/rag_query.ts +0 -107
- package/src/types/index.ts +0 -46
- package/src/utils/agentFetcher.ts +0 -107
- package/src/utils/conversation-utils.ts +0 -1
- package/src/utils/process.ts +0 -49
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolCallPart, ToolResultPart } from "ai";
|
|
1
2
|
import type { CompiledMessage } from "@/agents/execution/MessageCompiler";
|
|
2
3
|
import type { ConversationEntry } from "@/conversations/types";
|
|
3
4
|
import type {
|
|
@@ -258,13 +259,20 @@ export function validateSegmentsForEntries(
|
|
|
258
259
|
};
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
// Check for gaps between segments
|
|
262
|
+
// Check for gaps between segments.
|
|
263
|
+
// Non-eventId entries (tool-calls, tool-results) between two segments are fine:
|
|
264
|
+
// applySegmentsToEntries keeps them as-is in the output between the two summaries.
|
|
265
|
+
// Only flag a gap when eventId-bearing entries sit between segments and would
|
|
266
|
+
// be left uncompressed while we expected the LLM to cover the full range.
|
|
262
267
|
if (i > 0) {
|
|
263
268
|
const prevSegment = segments[i - 1];
|
|
264
269
|
const prevToIndex = rangeEntries.findIndex(
|
|
265
270
|
(e) => e.eventId === prevSegment.toEventId
|
|
266
271
|
);
|
|
267
|
-
|
|
272
|
+
const hasEventIdInGap = rangeEntries
|
|
273
|
+
.slice(prevToIndex + 1, fromIndex)
|
|
274
|
+
.some((e) => !!e.eventId);
|
|
275
|
+
if (hasEventIdInGap) {
|
|
268
276
|
return {
|
|
269
277
|
valid: false,
|
|
270
278
|
error: `Gap between segment ${i - 1} and ${i}`,
|
|
@@ -314,9 +322,11 @@ export function applySegments(
|
|
|
314
322
|
currentIndex++;
|
|
315
323
|
}
|
|
316
324
|
|
|
317
|
-
// Add compressed summary as
|
|
325
|
+
// Add compressed summary as user message
|
|
326
|
+
// NOTE: Cannot use "system" role here because Anthropic requires all system
|
|
327
|
+
// messages at the top of the prompt, not interleaved with user/assistant turns.
|
|
318
328
|
result.push({
|
|
319
|
-
role: "
|
|
329
|
+
role: "user",
|
|
320
330
|
content: `[Compressed history]\n${segment.compressed}`,
|
|
321
331
|
eventId: `compressed-${segment.fromEventId}-${segment.toEventId}`,
|
|
322
332
|
});
|
|
@@ -359,10 +369,12 @@ export function truncateSlidingWindow(
|
|
|
359
369
|
|
|
360
370
|
const kept = messages.slice(-windowSize);
|
|
361
371
|
|
|
362
|
-
// Add a
|
|
372
|
+
// Add a user message at the start indicating truncation
|
|
373
|
+
// NOTE: Cannot use "system" role because Anthropic requires all system
|
|
374
|
+
// messages at the top of the prompt, not interleaved with user/assistant turns.
|
|
363
375
|
return [
|
|
364
376
|
{
|
|
365
|
-
role: "
|
|
377
|
+
role: "user",
|
|
366
378
|
content: `[Earlier messages truncated. Showing last ${windowSize} messages.]`,
|
|
367
379
|
eventId: `truncated-${currentTimestamp}`,
|
|
368
380
|
},
|
|
@@ -407,15 +419,16 @@ export function applySegmentsToEntries(
|
|
|
407
419
|
currentIndex++;
|
|
408
420
|
}
|
|
409
421
|
|
|
410
|
-
// Add compressed summary as
|
|
411
|
-
//
|
|
422
|
+
// Add compressed summary as user entry with explicit role
|
|
423
|
+
// NOTE: Cannot use "system" role here because Anthropic requires all system
|
|
424
|
+
// messages at the top of the prompt, not interleaved with user/assistant turns.
|
|
412
425
|
result.push({
|
|
413
426
|
pubkey: "system",
|
|
414
427
|
content: `[Compressed history]\n${segment.compressed}`,
|
|
415
428
|
messageType: "text",
|
|
416
429
|
eventId: `compressed-${segment.fromEventId}-${segment.toEventId}`,
|
|
417
430
|
timestamp: segment.createdAt / 1000,
|
|
418
|
-
role: "
|
|
431
|
+
role: "user",
|
|
419
432
|
});
|
|
420
433
|
|
|
421
434
|
// Skip the compressed range
|
|
@@ -431,6 +444,18 @@ export function applySegmentsToEntries(
|
|
|
431
444
|
return result;
|
|
432
445
|
}
|
|
433
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Estimate the character count for a single conversation entry.
|
|
449
|
+
* Accounts for both text content and tool payloads (toolData).
|
|
450
|
+
*/
|
|
451
|
+
function estimateEntryChars(entry: ConversationEntry): number {
|
|
452
|
+
let chars = entry.content.length;
|
|
453
|
+
if (entry.toolData && entry.toolData.length > 0) {
|
|
454
|
+
chars += JSON.stringify(entry.toolData).length;
|
|
455
|
+
}
|
|
456
|
+
return chars;
|
|
457
|
+
}
|
|
458
|
+
|
|
434
459
|
/**
|
|
435
460
|
* Estimate token count for conversation entries using rough heuristic (chars/4).
|
|
436
461
|
* This is faster than actual tokenization and sufficient for compression decisions.
|
|
@@ -442,18 +467,35 @@ export function applySegmentsToEntries(
|
|
|
442
467
|
* @returns Estimated token count
|
|
443
468
|
*/
|
|
444
469
|
export function estimateTokensFromEntries(entries: ConversationEntry[]): number {
|
|
445
|
-
const totalChars = entries.reduce((sum, entry) =>
|
|
446
|
-
|
|
470
|
+
const totalChars = entries.reduce((sum, entry) => sum + estimateEntryChars(entry), 0);
|
|
471
|
+
return Math.ceil(totalChars / 4);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Compute how many trailing entries fit within a token budget.
|
|
476
|
+
* Walks entries backwards, accumulating estimated tokens, and stops when the budget is exhausted.
|
|
477
|
+
*
|
|
478
|
+
* @param entries - All conversation entries
|
|
479
|
+
* @param tokenBudget - Maximum tokens the trailing window should contain
|
|
480
|
+
* @returns Number of trailing entries that fit within the budget (minimum 1)
|
|
481
|
+
*/
|
|
482
|
+
export function computeTokenAwareWindowSize(
|
|
483
|
+
entries: ConversationEntry[],
|
|
484
|
+
tokenBudget: number
|
|
485
|
+
): number {
|
|
486
|
+
let accumulatedTokens = 0;
|
|
487
|
+
let count = 0;
|
|
447
488
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
489
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
490
|
+
const entryTokens = Math.ceil(estimateEntryChars(entries[i]) / 4);
|
|
491
|
+
if (accumulatedTokens + entryTokens > tokenBudget && count > 0) {
|
|
492
|
+
break;
|
|
452
493
|
}
|
|
494
|
+
accumulatedTokens += entryTokens;
|
|
495
|
+
count++;
|
|
496
|
+
}
|
|
453
497
|
|
|
454
|
-
|
|
455
|
-
}, 0);
|
|
456
|
-
return Math.ceil(totalChars / 4);
|
|
498
|
+
return Math.max(1, count);
|
|
457
499
|
}
|
|
458
500
|
|
|
459
501
|
/**
|
|
@@ -521,11 +563,14 @@ export function truncateSlidingWindowEntries(
|
|
|
521
563
|
const kept = entries.slice(-windowSize);
|
|
522
564
|
|
|
523
565
|
// Add a synthetic entry indicating truncation
|
|
566
|
+
// NOTE: Explicit role: "user" because Anthropic requires all system
|
|
567
|
+
// messages at the top of the prompt, not interleaved with user/assistant turns.
|
|
524
568
|
const truncationEntry: ConversationEntry = {
|
|
525
569
|
pubkey: "system",
|
|
526
570
|
content: `[Earlier messages truncated. Showing last ${windowSize} messages.]`,
|
|
527
571
|
messageType: "text",
|
|
528
572
|
timestamp: currentTimestamp / 1000,
|
|
573
|
+
role: "user",
|
|
529
574
|
};
|
|
530
575
|
|
|
531
576
|
return [truncationEntry, ...kept];
|
|
@@ -575,12 +620,58 @@ export function createFallbackSegmentForEntries(
|
|
|
575
620
|
}
|
|
576
621
|
}
|
|
577
622
|
|
|
623
|
+
// Ensure the boundary doesn't cut in the middle of a tool-call/tool-result pair.
|
|
624
|
+
// Collect tool-calls in [0, toEntry.index] that have no matching result in the same range.
|
|
625
|
+
const pendingCallIds = new Set<string>();
|
|
626
|
+
for (let i = 0; i <= toEntry.index; i++) {
|
|
627
|
+
const entry = entries[i];
|
|
628
|
+
if (entry.messageType === "tool-call" && entry.toolData) {
|
|
629
|
+
for (const part of entry.toolData as ToolCallPart[]) {
|
|
630
|
+
pendingCallIds.add(part.toolCallId);
|
|
631
|
+
}
|
|
632
|
+
} else if (entry.messageType === "tool-result" && entry.toolData) {
|
|
633
|
+
for (const part of entry.toolData as ToolResultPart[]) {
|
|
634
|
+
pendingCallIds.delete(part.toolCallId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// If there are unresolved tool-calls, their results live just after the boundary.
|
|
640
|
+
// Extend the segment past those orphaned tool-results so they are not left dangling.
|
|
641
|
+
if (pendingCallIds.size > 0) {
|
|
642
|
+
let i = toEntry.index + 1;
|
|
643
|
+
while (i < entries.length) {
|
|
644
|
+
const entry = entries[i];
|
|
645
|
+
if (
|
|
646
|
+
entry.messageType === "tool-result" &&
|
|
647
|
+
entry.toolData &&
|
|
648
|
+
(entry.toolData as ToolResultPart[]).some((p) => pendingCallIds.has(p.toolCallId))
|
|
649
|
+
) {
|
|
650
|
+
for (const p of entry.toolData as ToolResultPart[]) {
|
|
651
|
+
pendingCallIds.delete(p.toolCallId);
|
|
652
|
+
}
|
|
653
|
+
i++;
|
|
654
|
+
} else {
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Find the next entry with an eventId at or after position i to use as the new boundary.
|
|
659
|
+
for (let j = i; j < entries.length; j++) {
|
|
660
|
+
if (entries[j].eventId) {
|
|
661
|
+
toEntry = { entry: entries[j], index: j };
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// If no eventId is found we keep the original toEntry; MessageBuilder will skip
|
|
666
|
+
// any remaining orphaned tool-results as a safety net.
|
|
667
|
+
}
|
|
668
|
+
|
|
578
669
|
const toEventId = toEntry.entry.eventId!;
|
|
579
670
|
|
|
580
671
|
return {
|
|
581
672
|
fromEventId,
|
|
582
673
|
toEventId,
|
|
583
|
-
compressed: `[Truncated ${
|
|
674
|
+
compressed: `[Truncated ${toEntry.index + 1} earlier messages due to compression failure]`,
|
|
584
675
|
createdAt: Date.now(),
|
|
585
676
|
model: "fallback-truncation",
|
|
586
677
|
};
|
|
@@ -192,8 +192,6 @@ export interface MetaModelVariant {
|
|
|
192
192
|
description?: string;
|
|
193
193
|
/** Optional additional system prompt to inject when this variant is active */
|
|
194
194
|
systemPrompt?: string;
|
|
195
|
-
/** Priority tier for conflict resolution (higher number = higher priority) */
|
|
196
|
-
tier?: number;
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
/**
|
|
@@ -207,8 +205,6 @@ export interface MetaModelConfiguration {
|
|
|
207
205
|
variants: Record<string, MetaModelVariant>;
|
|
208
206
|
/** Default variant to use when no keyword matches */
|
|
209
207
|
default: string;
|
|
210
|
-
/** Optional description shown in system prompt preamble */
|
|
211
|
-
description?: string;
|
|
212
208
|
}
|
|
213
209
|
|
|
214
210
|
/**
|
|
@@ -258,7 +254,6 @@ export const MetaModelVariantSchema = z.object({
|
|
|
258
254
|
keywords: z.array(z.string()).optional(),
|
|
259
255
|
description: z.string().optional(),
|
|
260
256
|
systemPrompt: z.string().optional(),
|
|
261
|
-
tier: z.number().optional(),
|
|
262
257
|
});
|
|
263
258
|
|
|
264
259
|
/**
|
|
@@ -268,7 +263,6 @@ export const MetaModelConfigurationSchema = z.object({
|
|
|
268
263
|
provider: z.literal("meta"),
|
|
269
264
|
variants: z.record(z.string(), MetaModelVariantSchema),
|
|
270
265
|
default: z.string(),
|
|
271
|
-
description: z.string().optional(),
|
|
272
266
|
});
|
|
273
267
|
|
|
274
268
|
/**
|
|
@@ -92,6 +92,19 @@ export class AgentDispatchService {
|
|
|
92
92
|
error: formatAnyError(error),
|
|
93
93
|
eventId: event.id,
|
|
94
94
|
});
|
|
95
|
+
logger.writeToWarnLog({
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
level: "error",
|
|
98
|
+
component: "AgentDispatchService",
|
|
99
|
+
message: "Failed to route incoming reply",
|
|
100
|
+
context: {
|
|
101
|
+
eventId: event.id,
|
|
102
|
+
eventKind: event.kind,
|
|
103
|
+
pubkey: event.pubkey,
|
|
104
|
+
},
|
|
105
|
+
error: formatAnyError(error),
|
|
106
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
107
|
+
});
|
|
95
108
|
} finally {
|
|
96
109
|
span.end();
|
|
97
110
|
}
|
|
@@ -372,6 +385,46 @@ export class AgentDispatchService {
|
|
|
372
385
|
"ral.is_streaming": activeRal.isStreaming,
|
|
373
386
|
});
|
|
374
387
|
}
|
|
388
|
+
|
|
389
|
+
// IMMEDIATE MARKER UPDATE: Update delegation markers in ConversationStore
|
|
390
|
+
// right away, BEFORE the debounce. This ensures that if the executor's
|
|
391
|
+
// supervision re-engages and recompiles messages during the debounce window,
|
|
392
|
+
// it sees "DELEGATION COMPLETED" instead of stale "DELEGATION IN PROGRESS".
|
|
393
|
+
// The debounce still handles re-execution triggering and clearCompletedDelegations.
|
|
394
|
+
if (activeRal) {
|
|
395
|
+
const completedDelegations = ralRegistry.getConversationCompletedDelegations(
|
|
396
|
+
delegationTarget.agent.pubkey,
|
|
397
|
+
delegationTarget.conversationId,
|
|
398
|
+
activeRal.ralNumber
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const parentStore = ConversationStore.get(delegationTarget.conversationId);
|
|
402
|
+
if (parentStore && completedDelegations.length > 0) {
|
|
403
|
+
let markersUpdated = 0;
|
|
404
|
+
for (const completion of completedDelegations) {
|
|
405
|
+
const updated = parentStore.updateDelegationMarker(
|
|
406
|
+
completion.delegationConversationId,
|
|
407
|
+
{
|
|
408
|
+
status: completion.status,
|
|
409
|
+
completedAt: completion.completedAt,
|
|
410
|
+
abortReason: completion.status === "aborted" ? completion.abortReason : undefined,
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
if (updated) markersUpdated++;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (markersUpdated > 0) {
|
|
417
|
+
await parentStore.save();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
span.addEvent("dispatch.markers_updated_before_debounce", {
|
|
421
|
+
"ral.number": activeRal.ralNumber,
|
|
422
|
+
"delegation.completed_count": completedDelegations.length,
|
|
423
|
+
"markers.updated_count": markersUpdated,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
375
428
|
const debounceKey = `${delegationTarget.agent.pubkey}:${delegationTarget.conversationId}`;
|
|
376
429
|
const debounceSequence = await this.waitForDelegationDebounce(debounceKey, span);
|
|
377
430
|
if (this.delegationDebounceSequence.get(debounceKey) !== debounceSequence) {
|
|
@@ -535,6 +588,19 @@ export class AgentDispatchService {
|
|
|
535
588
|
code: SpanStatusCode.ERROR,
|
|
536
589
|
message: (error as Error).message,
|
|
537
590
|
});
|
|
591
|
+
logger.writeToWarnLog({
|
|
592
|
+
timestamp: new Date().toISOString(),
|
|
593
|
+
level: "error",
|
|
594
|
+
component: "AgentDispatchService",
|
|
595
|
+
message: "Delegation routing execution failed",
|
|
596
|
+
context: {
|
|
597
|
+
agentSlug: delegationTarget.agent.slug,
|
|
598
|
+
conversationId: delegationTarget.conversationId,
|
|
599
|
+
triggerEventId: event.id,
|
|
600
|
+
},
|
|
601
|
+
error: error instanceof Error ? error.message : String(error),
|
|
602
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
603
|
+
});
|
|
538
604
|
throw error;
|
|
539
605
|
} finally {
|
|
540
606
|
span.end();
|
|
@@ -705,6 +771,19 @@ export class AgentDispatchService {
|
|
|
705
771
|
code: SpanStatusCode.ERROR,
|
|
706
772
|
message: (error as Error).message,
|
|
707
773
|
});
|
|
774
|
+
logger.writeToWarnLog({
|
|
775
|
+
timestamp: new Date().toISOString(),
|
|
776
|
+
level: "error",
|
|
777
|
+
component: "AgentDispatchService",
|
|
778
|
+
message: "Agent execution failed during dispatch",
|
|
779
|
+
context: {
|
|
780
|
+
agentSlug: targetAgent.slug,
|
|
781
|
+
conversationId,
|
|
782
|
+
triggerEventId: event.id,
|
|
783
|
+
},
|
|
784
|
+
error: error instanceof Error ? error.message : String(error),
|
|
785
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
786
|
+
});
|
|
708
787
|
throw error;
|
|
709
788
|
} finally {
|
|
710
789
|
agentSpan.end();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { InterventionPublisher } from "@/nostr/InterventionPublisher";
|
|
4
|
+
import { NDKKind } from "@/nostr/kinds";
|
|
4
5
|
import { config } from "@/services/ConfigService";
|
|
5
6
|
import { PubkeyService } from "@/services/PubkeyService";
|
|
6
7
|
import { getTrustPubkeyService } from "@/services/trust-pubkeys/TrustPubkeyService";
|
|
@@ -76,7 +77,8 @@ interface NotifiedEntry {
|
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* Persisted state for InterventionService.
|
|
79
|
-
* Stored in ~/.tenex/intervention_state_<
|
|
80
|
+
* Stored in ~/.tenex/intervention_state_<stateScope>.json (project-scoped).
|
|
81
|
+
* For NIP-33 project coordinates, stateScope is the project's dTag.
|
|
80
82
|
*/
|
|
81
83
|
interface InterventionState {
|
|
82
84
|
pending: PendingIntervention[];
|
|
@@ -176,12 +178,70 @@ export class InterventionService {
|
|
|
176
178
|
|
|
177
179
|
/**
|
|
178
180
|
* Get the state file path for a given project.
|
|
179
|
-
* State files are scoped by
|
|
181
|
+
* State files are scoped by dTag when projectId is a NIP-33 coordinate.
|
|
180
182
|
*/
|
|
181
183
|
private getStateFilePath(projectId: string): string {
|
|
184
|
+
const stateScope = this.getStateScope(projectId);
|
|
185
|
+
return path.join(this.configDir, `intervention_state_${stateScope}.json`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Derive stable state scope from a project identifier.
|
|
190
|
+
* Project coordinates use format: "<kind>:<authorPubkey>:<dTag>".
|
|
191
|
+
*/
|
|
192
|
+
private getStateScope(projectId: string): string {
|
|
193
|
+
const parts = projectId.split(":");
|
|
194
|
+
const isProjectCoordinate = parts.length >= 3 && parts[0] === String(NDKKind.Project);
|
|
195
|
+
if (!isProjectCoordinate) {
|
|
196
|
+
return projectId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const dTag = parts.slice(2).join(":");
|
|
200
|
+
return dTag || projectId;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Legacy path used before dTag-scoped filenames.
|
|
205
|
+
* Returns null when projectId already represents the state scope.
|
|
206
|
+
*/
|
|
207
|
+
private getLegacyStateFilePath(projectId: string): string | null {
|
|
208
|
+
const stateScope = this.getStateScope(projectId);
|
|
209
|
+
if (stateScope === projectId) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
182
212
|
return path.join(this.configDir, `intervention_state_${projectId}.json`);
|
|
183
213
|
}
|
|
184
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Read state file from canonical location, with fallback to legacy path.
|
|
217
|
+
*/
|
|
218
|
+
private async readStateFile(projectId: string): Promise<{ data: string; loadedFromLegacyPath: boolean } | null> {
|
|
219
|
+
const stateFilePath = this.getStateFilePath(projectId);
|
|
220
|
+
try {
|
|
221
|
+
const data = await fs.readFile(stateFilePath, "utf-8");
|
|
222
|
+
return { data, loadedFromLegacyPath: false };
|
|
223
|
+
} catch (error: unknown) {
|
|
224
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const legacyStateFilePath = this.getLegacyStateFilePath(projectId);
|
|
230
|
+
if (!legacyStateFilePath) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const data = await fs.readFile(legacyStateFilePath, "utf-8");
|
|
236
|
+
return { data, loadedFromLegacyPath: true };
|
|
237
|
+
} catch (error: unknown) {
|
|
238
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
185
245
|
/**
|
|
186
246
|
* Get the singleton instance of InterventionService.
|
|
187
247
|
*/
|
|
@@ -996,8 +1056,6 @@ export class InterventionService {
|
|
|
996
1056
|
* Load persisted state from disk for the given project.
|
|
997
1057
|
*/
|
|
998
1058
|
private async loadState(projectId: string): Promise<void> {
|
|
999
|
-
const stateFilePath = this.getStateFilePath(projectId);
|
|
1000
|
-
|
|
1001
1059
|
// Cancel all active timers from the previous project before clearing state.
|
|
1002
1060
|
// Without this, timers from a previous project continue running and fire
|
|
1003
1061
|
// against the new project's data, causing duplicate notifications and
|
|
@@ -1014,7 +1072,15 @@ export class InterventionService {
|
|
|
1014
1072
|
this.triggeringConversations.clear();
|
|
1015
1073
|
|
|
1016
1074
|
try {
|
|
1017
|
-
const
|
|
1075
|
+
const stateFile = await this.readStateFile(projectId);
|
|
1076
|
+
if (!stateFile) {
|
|
1077
|
+
logger.debug("No existing intervention state file, starting fresh", {
|
|
1078
|
+
projectId: projectId.substring(0, 12),
|
|
1079
|
+
});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const data = stateFile.data;
|
|
1018
1084
|
const state = JSON.parse(data) as InterventionState;
|
|
1019
1085
|
|
|
1020
1086
|
for (const pending of state.pending) {
|
|
@@ -1039,13 +1105,20 @@ export class InterventionService {
|
|
|
1039
1105
|
projectId: projectId.substring(0, 12),
|
|
1040
1106
|
pendingCount: this.pendingInterventions.size,
|
|
1041
1107
|
notifiedCount: this.notifiedConversations.size,
|
|
1108
|
+
loadedFromLegacyPath: stateFile.loadedFromLegacyPath,
|
|
1042
1109
|
});
|
|
1043
1110
|
|
|
1044
1111
|
trace.getActiveSpan()?.addEvent("intervention.state_loaded", {
|
|
1045
1112
|
"intervention.project_id": projectId.substring(0, 12),
|
|
1046
1113
|
"intervention.pending_count": this.pendingInterventions.size,
|
|
1047
1114
|
"intervention.notified_count": this.notifiedConversations.size,
|
|
1115
|
+
"intervention.loaded_from_legacy_path": stateFile.loadedFromLegacyPath,
|
|
1048
1116
|
});
|
|
1117
|
+
|
|
1118
|
+
// Persist back using canonical (dTag-scoped) filename after legacy load.
|
|
1119
|
+
if (stateFile.loadedFromLegacyPath) {
|
|
1120
|
+
this.saveState();
|
|
1121
|
+
}
|
|
1049
1122
|
} catch (error: unknown) {
|
|
1050
1123
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1051
1124
|
// No existing file, starting fresh - this is expected
|
|
@@ -89,7 +89,7 @@ export class Nip46SigningService {
|
|
|
89
89
|
isEnabled(): boolean {
|
|
90
90
|
try {
|
|
91
91
|
const cfg = config.getConfig();
|
|
92
|
-
return cfg.nip46?.enabled
|
|
92
|
+
return cfg.nip46?.enabled !== false;
|
|
93
93
|
} catch {
|
|
94
94
|
return false;
|
|
95
95
|
}
|
|
@@ -233,6 +233,19 @@ export class Nip46SigningService {
|
|
|
233
233
|
durationMs,
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
+
logger.writeToWarnLog({
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
level: "error",
|
|
239
|
+
component: "Nip46SigningService",
|
|
240
|
+
message: "NIP-46 signer connection failed",
|
|
241
|
+
context: {
|
|
242
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
243
|
+
durationMs,
|
|
244
|
+
},
|
|
245
|
+
error: errorMsg,
|
|
246
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
247
|
+
});
|
|
248
|
+
|
|
236
249
|
this.signingLog.log({
|
|
237
250
|
op: "sign_error",
|
|
238
251
|
requestId,
|
|
@@ -430,6 +443,22 @@ export class Nip46SigningService {
|
|
|
430
443
|
durationMs,
|
|
431
444
|
});
|
|
432
445
|
|
|
446
|
+
logger.writeToWarnLog({
|
|
447
|
+
timestamp: new Date().toISOString(),
|
|
448
|
+
level: "error",
|
|
449
|
+
component: "Nip46SigningService",
|
|
450
|
+
message: `NIP-46 signing failed after ${maxRetries} retries`,
|
|
451
|
+
context: {
|
|
452
|
+
ownerPubkey: ownerPubkey.substring(0, 12),
|
|
453
|
+
eventKind: event.kind,
|
|
454
|
+
retries: maxRetries,
|
|
455
|
+
durationMs,
|
|
456
|
+
isTimeout: errorMsg.includes("timed out"),
|
|
457
|
+
},
|
|
458
|
+
error: errorMsg,
|
|
459
|
+
stack: lastError?.stack,
|
|
460
|
+
});
|
|
461
|
+
|
|
433
462
|
return { outcome: "failed", reason: errorMsg };
|
|
434
463
|
}
|
|
435
464
|
|
|
@@ -101,9 +101,10 @@ export function resolveProjectManager(
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
logger.warn("PM agent designated in project tags not loaded in registry yet, falling through", {
|
|
105
|
+
pmEventId,
|
|
106
|
+
loadedEventIds: Array.from(agents.values()).map(a => a.eventId),
|
|
107
|
+
});
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
// Step 5: Fallback to first agent from project tags
|
|
@@ -121,9 +122,10 @@ export function resolveProjectManager(
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
logger.warn("First agent from project tags not loaded in registry yet, falling through", {
|
|
126
|
+
pmEventId,
|
|
127
|
+
loadedEventIds: Array.from(agents.values()).map(a => a.eventId),
|
|
128
|
+
});
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// Step 6: No agent tags in project, use first from registry if any exist
|