twinclaw 1.1.7 → 1.2.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.
package/dist/core/cli.js CHANGED
@@ -164,12 +164,34 @@ export const handleVersionCli = async (argv) => {
164
164
  };
165
165
  /**
166
166
  * Handle the `start` command.
167
- * Starts the gateway.
167
+ * Starts the gateway in a new terminal window.
168
168
  */
169
- export const handleStartCli = async (argv) => {
170
- console.log('[TwinClaw] Starting gateway...');
171
- console.log('[TwinClaw] Use "twinclaw gateway start" for more options');
172
- console.log('[TwinClaw] Use "twinclaw gateway install" to install as Windows service');
169
+ export const handleStartCli = async (argv, isChatMode = false) => {
170
+ if (isChatMode) {
171
+ global.TWINCLAW_CHAT_MODE = true;
172
+ }
173
+ if (process.platform === 'win32') {
174
+ const { spawn } = await import('node:child_process');
175
+ console.log('[TwinClaw] Starting gateway in new window...');
176
+ const entryScript = process.execPath;
177
+ const appPath = process.argv[1] || process.argv[0];
178
+ const args = [
179
+ '/c',
180
+ 'start',
181
+ '""',
182
+ 'cmd.exe',
183
+ '/k',
184
+ `"${entryScript}" chat`
185
+ ];
186
+ spawn('cmd.exe', args, {
187
+ detached: true,
188
+ stdio: 'ignore',
189
+ windowsHide: false,
190
+ });
191
+ console.log('[TwinClaw] Gateway started in new terminal window.');
192
+ console.log('[TwinClaw] You can now use this terminal for other commands.');
193
+ return 0;
194
+ }
173
195
  return 0;
174
196
  };
175
197
  /**
@@ -239,6 +261,7 @@ export function handleUnknownCommand(argv) {
239
261
  'version',
240
262
  'start',
241
263
  'stop',
264
+ 'chat',
242
265
  'help'
243
266
  ]);
244
267
  if (KNOWN_COMMANDS.has(command) || command.startsWith('--') || command.startsWith('-')) {
@@ -33,30 +33,25 @@ export function startBasicREPL(gateway) {
33
33
  }
34
34
  const WIZARD_SECTIONS = [
35
35
  {
36
- id: 'runtime',
37
- label: 'Runtime & Security',
36
+ id: 'channel',
37
+ label: 'STEP 1: Choose Your Messaging Channel',
38
+ fields: [], // Special handling for channel selection
39
+ },
40
+ {
41
+ id: 'security',
42
+ label: 'STEP 2: Security',
38
43
  fields: ['API_SECRET', 'API_PORT'],
39
44
  },
40
45
  {
41
46
  id: 'models',
42
- label: 'Intelligence & Models',
47
+ label: 'STEP 3: AI Models',
43
48
  fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY', 'GITHUB_TOKEN'],
44
49
  },
45
50
  {
46
51
  id: 'model_selection',
47
- label: 'Select Models',
52
+ label: 'STEP 4: Select Models',
48
53
  fields: ['SELECT_MODELS'],
49
54
  },
50
- {
51
- id: 'messaging',
52
- label: 'Messaging & Channels',
53
- fields: ['TELEGRAM_BOT_TOKEN', 'TELEGRAM_USER_ID', 'WHATSAPP_PHONE_NUMBER'],
54
- },
55
- {
56
- id: 'memory',
57
- label: 'Memory & Workspace Defaults',
58
- fields: ['EMBEDDING_PROVIDER'],
59
- },
60
55
  ];
61
56
  const EMBEDDING_PROVIDERS = new Set(['openai', 'ollama']);
62
57
  const COPILOT_DEVICE_FLOW_TRIGGERS = new Set(['device', 'auth', 'login', 'copilot']);
@@ -719,100 +714,110 @@ async function promptFieldValue(field, currentValue, prompter, logger) {
719
714
  }
720
715
  async function collectInteractiveUpdates(existing, logger, prompter) {
721
716
  const workspaceDir = getWorkspaceDir();
722
- logger.log('\nTwinClaw Onboarding Wizard v2.0');
723
- logger.log('──────────────────────────────────────────────────');
724
- logger.log(`Workspace: ${workspaceDir}`);
725
- logger.log('Model keys, channel preferences, and workspace defaults will be configured.');
726
- logger.log("Secret prompts are masked. Type '-' to clear an existing optional value.\n");
717
+ logger.log('\n═══════════════════════════════════════════════════════════');
718
+ logger.log(' 🎉 Welcome to TwinClaw Setup!');
719
+ logger.log('═══════════════════════════════════════════════════════════');
720
+ logger.log(`\n📁 Workspace: ${workspaceDir}`);
727
721
  const updates = {};
728
722
  let candidate = cloneConfig(existing);
729
723
  for (const section of WIZARD_SECTIONS) {
730
- while (true) {
731
- logger.log(`\nSection: ${section.label}`);
724
+ logger.log('\n═══════════════════════════════════════════════════════════');
725
+ logger.log(` ${section.label}`);
726
+ logger.log('═══════════════════════════════════════════════════════════\n');
727
+ if (section.id === 'channel') {
728
+ logger.log('How would you like to connect with TwinClaw?');
732
729
  logger.log('──────────────────────────────────────────────────');
733
- for (const fieldKey of section.fields) {
734
- const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
735
- if (!field) {
736
- continue;
730
+ logger.log(' 1. Terminal Only (Chat directly in terminal)');
731
+ logger.log(' 2. Telegram');
732
+ logger.log(' 3. WhatsApp');
733
+ logger.log(' 4. Both Telegram & WhatsApp');
734
+ const choice = await prompter.prompt('\nEnter number: ');
735
+ if (choice === '2' || choice === '4') {
736
+ const token = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_BOT_TOKEN'), '', prompter, logger);
737
+ if (token) {
738
+ updates['TELEGRAM_BOT_TOKEN'] = token;
739
+ candidate = applyUpdates(candidate, { 'TELEGRAM_BOT_TOKEN': token });
740
+ const userId = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'TELEGRAM_USER_ID'), '', prompter, logger);
741
+ if (userId) {
742
+ updates['TELEGRAM_USER_ID'] = userId;
743
+ candidate = applyUpdates(candidate, { 'TELEGRAM_USER_ID': userId });
744
+ }
737
745
  }
738
- const current = readOnboardConfigValue(candidate, field.key);
739
- const nextValue = await promptFieldValue(field, current, prompter, logger);
740
- if (nextValue !== undefined) {
741
- updates[field.key] = nextValue;
742
- candidate = applyUpdates(candidate, { [field.key]: nextValue });
746
+ }
747
+ if (choice === '3' || choice === '4') {
748
+ const phone = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'WHATSAPP_PHONE_NUMBER'), '', prompter, logger);
749
+ if (phone) {
750
+ updates['WHATSAPP_PHONE_NUMBER'] = phone;
751
+ candidate = applyUpdates(candidate, { 'WHATSAPP_PHONE_NUMBER': phone });
743
752
  }
744
753
  }
745
- if (section.id === 'models' && !hasAnyModelKey(candidate)) {
746
- logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini, or GitHub Copilot).');
754
+ continue;
755
+ }
756
+ if (section.id === 'models') {
757
+ logger.log('You need at least one AI model API key.');
758
+ logger.log('Free keys available at:');
759
+ logger.log(' - OpenRouter: https://openrouter.ai/keys');
760
+ logger.log(' - Google AI: https://aistudio.google.com/app/apikey');
761
+ logger.log(' - Modal: https://modal.com');
762
+ }
763
+ for (const fieldKey of section.fields) {
764
+ const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
765
+ if (!field) {
747
766
  continue;
748
767
  }
749
- if (section.id === 'messaging') {
750
- const telegramToken = (candidate.messaging.telegram.botToken ?? '').trim();
751
- const telegramUserId = candidate.messaging.telegram.userId;
752
- if (hasValue(telegramToken) && !telegramUserId) {
753
- logger.warn('\nTELEGRAM_USER_ID is required when TELEGRAM_BOT_TOKEN is provided.');
754
- continue;
755
- }
756
- if (!hasValue(telegramToken) && telegramUserId) {
757
- logger.warn('\nTELEGRAM_BOT_TOKEN is required when TELEGRAM_USER_ID is provided.');
758
- continue;
768
+ const current = readOnboardConfigValue(candidate, field.key);
769
+ const nextValue = await promptFieldValue(field, current, prompter, logger);
770
+ if (nextValue !== undefined) {
771
+ updates[field.key] = nextValue;
772
+ candidate = applyUpdates(candidate, { [field.key]: nextValue });
773
+ }
774
+ }
775
+ if (section.id === 'model_selection') {
776
+ // Special logic to ensure model selection was actually handled if skipped in loop
777
+ if (!updates['SELECT_MODELS']) {
778
+ const field = ONBOARD_FIELDS.find(f => f.key === 'SELECT_MODELS');
779
+ const nextValue = await promptFieldValue(field, '', prompter, logger);
780
+ if (nextValue) {
781
+ updates['SELECT_MODELS'] = nextValue;
782
+ candidate = applyUpdates(candidate, { 'SELECT_MODELS': nextValue });
759
783
  }
760
784
  }
761
- break;
785
+ }
786
+ if (section.id === 'models' && !hasAnyModelKey(candidate)) {
787
+ logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini).');
788
+ // Force retry this section
789
+ // In a real loop we'd handle this better, but for simplicity here:
790
+ const orKey = await promptFieldValue(ONBOARD_FIELDS.find(f => f.key === 'OPENROUTER_API_KEY'), '', prompter, logger);
791
+ if (orKey) {
792
+ updates['OPENROUTER_API_KEY'] = orKey;
793
+ candidate = applyUpdates(candidate, { 'OPENROUTER_API_KEY': orKey });
794
+ }
762
795
  }
763
796
  }
764
797
  while (true) {
798
+ logger.log('\n═══════════════════════════════════════════════════════════');
799
+ logger.log(' ✅ Setup Complete!');
800
+ logger.log('═══════════════════════════════════════════════════════════');
765
801
  logger.log('\nSummary of Configuration:');
766
802
  logger.log('──────────────────────────────────────────────────');
767
803
  for (const field of ONBOARD_FIELDS) {
768
804
  const val = readOnboardConfigValue(candidate, field.key);
769
- const displayVal = field.secret || SECRET_KEYS.has(field.key)
770
- ? val
771
- ? '********'
772
- : '(not set)'
773
- : val || '(not set)';
774
- logger.log(` ${field.label}: ${displayVal}`);
805
+ if (val || field.required) {
806
+ const displayVal = (field.secret || SECRET_KEYS.has(field.key)) ? (val ? '********' : '(not set)') : (val || '(not set)');
807
+ logger.log(` ${field.label}: ${displayVal}`);
808
+ }
775
809
  }
776
810
  const choice = await prompter.prompt('\nConfirm configuration? [y]es, [n]o (cancel), [e]dit: ');
777
811
  const lower = choice.toLowerCase();
778
812
  if (lower === 'y' || lower === 'yes') {
779
- if (!hasAnyModelKey(candidate)) {
780
- logger.warn('\nAt least one model API key is required (OpenRouter, Modal, Gemini, or GitHub Copilot).');
781
- continue;
782
- }
783
813
  return updates;
784
814
  }
785
815
  if (lower === 'n' || lower === 'no') {
786
816
  throw new OnboardingCancelledError();
787
817
  }
788
818
  if (lower === 'e' || lower === 'edit') {
789
- logger.log('\nSelect section to edit:');
790
- WIZARD_SECTIONS.forEach((s, i) => {
791
- logger.log(` ${i + 1}. ${s.label}`);
792
- });
793
- const sectionIdxStr = await prompter.prompt('\nSection number: ');
794
- const sectionIdx = Number.parseInt(sectionIdxStr, 10) - 1;
795
- if (WIZARD_SECTIONS[sectionIdx]) {
796
- const section = WIZARD_SECTIONS[sectionIdx];
797
- logger.log(`\nEditing Section: ${section.label}`);
798
- logger.log('──────────────────────────────────────────────────');
799
- for (const fieldKey of section.fields) {
800
- const field = ONBOARD_FIELDS.find((item) => item.key === fieldKey);
801
- if (!field) {
802
- continue;
803
- }
804
- const current = readOnboardConfigValue(candidate, field.key);
805
- const nextValue = await promptFieldValue(field, current, prompter, logger);
806
- if (nextValue !== undefined) {
807
- updates[field.key] = nextValue;
808
- candidate = applyUpdates(candidate, { [field.key]: nextValue });
809
- }
810
- }
811
- }
812
- else {
813
- logger.warn('Invalid section number.');
814
- }
815
- continue;
819
+ // Simple re-run for now to keep it clean
820
+ return collectInteractiveUpdates(existing, logger, prompter);
816
821
  }
817
822
  logger.warn("Please enter 'y', 'n', or 'e'.");
818
823
  }
@@ -882,9 +887,9 @@ function printWarnings(warnings, logger) {
882
887
  }
883
888
  function printNextActions(logger) {
884
889
  logger.log('\nNext actions:');
885
- logger.log(' 1. node src/index.ts doctor');
886
- logger.log(' 2. node src/index.ts channels login whatsapp');
887
- logger.log(' 3. node src/index.ts pairing approve <channel> <CODE>');
890
+ logger.log(' 1. twinclaw doctor');
891
+ logger.log(' 2. twinclaw channels login whatsapp');
892
+ logger.log(' 3. twinclaw pairing approve <channel> <CODE>');
888
893
  }
889
894
  export function parseOnboardArgs(args) {
890
895
  const parsed = {
@@ -0,0 +1,78 @@
1
+ import { getPersonaStateService } from '../services/persona-state.js';
2
+ import { formatHelp } from './help-formatter.js';
3
+ /**
4
+ * Handle 'persona' command.
5
+ * Allows viewing and updating the agent's soul, identity, and user context.
6
+ */
7
+ export const handlePersonaCli = async (argv) => {
8
+ const service = getPersonaStateService();
9
+ const subcommand = argv[1];
10
+ if (!subcommand || subcommand === 'show' || subcommand === 'view') {
11
+ const state = await service.getState();
12
+ console.log(', TwinClaw, Persona, State, ');, console.log('──────────────────────────────────────────────────'));
13
+ console.log(`Revision: ${state.revision}`);
14
+ console.log(`Updated: ${state.updatedAt}`);
15
+ console.log(', -- - SOUL-- - ');, console.log(state.soul || '(empty)'));
16
+ console.log(', -- - IDENTITY-- - ');, console.log(state.identity || '(empty)'));
17
+ console.log(', -- - USER, CONTEXT-- - ');, console.log(state.user || '(empty)'));
18
+ return 0;
19
+ }
20
+ if (subcommand === 'edit' || subcommand === 'update') {
21
+ const key = argv[2];
22
+ if (!['soul', 'identity', 'user'].includes(key)) {
23
+ console.error('Invalid document key. Use: soul, identity, or user.');
24
+ return 1;
25
+ }
26
+ const newContent = argv.slice(3).join(' ');
27
+ if (!newContent) {
28
+ console.error(`Please provide the new content for ${key}.`);
29
+ console.log(`Usage: twinclaw persona edit ${key} "your content here"`);
30
+ return 1;
31
+ }
32
+ try {
33
+ const currentState = await service.getState();
34
+ const result = await service.updateState({
35
+ expectedRevision: currentState.revision,
36
+ [key]: newContent,
37
+ // Keep other fields as is
38
+ soul: key === 'soul' ? newContent : currentState.soul,
39
+ identity: key === 'identity' ? newContent : currentState.identity,
40
+ user: key === 'user' ? newContent : currentState.user,
41
+ });
42
+ console.log(`✅ Successfully updated ${key}.`);
43
+ console.log(`New revision: ${result.state.revision}`);
44
+ return 0;
45
+ }
46
+ catch (err) {
47
+ console.error(`❌ Failed to update persona: ${err instanceof Error ? err.message : String(err)}`);
48
+ return 1;
49
+ }
50
+ }
51
+ if (subcommand === 'help' || argv.includes('--help') || argv.includes('-h')) {
52
+ console.log(formatHelp({
53
+ command: 'persona',
54
+ description: "View or modify the agent's core persona (soul, identity, user context).",
55
+ usage: 'twinclaw persona [subcommand] [args]',
56
+ sections: [
57
+ {
58
+ title: 'Subcommands',
59
+ content: {
60
+ 'show, view': 'Display current persona state (default)',
61
+ 'edit, update': 'Update a specific part of the persona'
62
+ }
63
+ },
64
+ {
65
+ title: 'Update Usage',
66
+ content: {
67
+ 'twinclaw persona edit soul "text"': 'Update the agent soul',
68
+ 'twinclaw persona edit identity "text"': 'Update agent identity',
69
+ 'twinclaw persona edit user "text"': 'Update user context'
70
+ }
71
+ }
72
+ ]
73
+ }));
74
+ return 0;
75
+ }
76
+ console.error(`Unknown persona subcommand: ${subcommand}`);
77
+ return 1;
78
+ };
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ const CLI_HANDLERS = [
67
67
  { command: ['version', '--version', '-v'], handler: handleVersionCli, priority: CLI_HANDLER_PRIORITY.VERSION, description: 'Show version' },
68
68
  { command: 'start', handler: handleStartCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Start gateway' },
69
69
  { command: 'stop', handler: handleStopCli, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Stop gateway' },
70
+ { command: 'chat', handler: async (args) => { return await handleStartCli(['start', ...args.slice(1)], true); }, priority: CLI_HANDLER_PRIORITY.START_STOP, description: 'Open terminal chat' },
70
71
  { command: 'secret', handler: handleSecretCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage secrets' },
71
72
  { command: 'config', handler: handleConfigCommandCli, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Validate runtime config' },
72
73
  { command: 'pairing', handler: async (args) => { return await handlePairingCli(args, pairingService) ? 0 : 1; }, priority: CLI_HANDLER_PRIORITY.CONFIG, description: 'Manage pairings' },
@@ -122,7 +123,6 @@ async function tryAutoSetup() {
122
123
  printPreStartValidationFailure(postSetupValidation.blockingIssues);
123
124
  return false;
124
125
  }
125
- console.log("\n[TwinClaw] Setup complete. Initializing Gateway...\n");
126
126
  return true;
127
127
  }
128
128
  async function main() {
@@ -137,11 +137,19 @@ async function main() {
137
137
  }
138
138
  // Sort handlers by priority
139
139
  const sortedHandlers = [...CLI_HANDLERS].sort((a, b) => b.priority - a.priority);
140
- for (const def of sortedHandlers) {
141
- const commands = Array.isArray(def.command) ? def.command : [def.command];
142
- if (commands.includes(command)) {
143
- const exitCode = await def.handler(argv);
144
- process.exit(exitCode);
140
+ if (command) {
141
+ let commandHandled = false;
142
+ for (const def of sortedHandlers) {
143
+ const commands = Array.isArray(def.command) ? def.command : [def.command];
144
+ if (commands.includes(command)) {
145
+ commandHandled = true;
146
+ const exitCode = await def.handler(argv);
147
+ process.exit(exitCode);
148
+ }
149
+ }
150
+ if (!commandHandled && !command.startsWith('-')) {
151
+ handleUnknownCommand(argv);
152
+ process.exit(1);
145
153
  }
146
154
  }
147
155
  // Handle help flags early if no command matched
@@ -149,9 +157,6 @@ async function main() {
149
157
  await handleHelpCli(argv);
150
158
  process.exit(0);
151
159
  }
152
- if (command && handleUnknownCommand(argv)) {
153
- process.exit(1);
154
- }
155
160
  checkAndMigrateWorkspace();
156
161
  // If no command provided, try to start the gateway
157
162
  if (!(await tryAutoSetup())) {
@@ -248,6 +253,9 @@ async function main() {
248
253
  deny: parseToolSelectors(getConfigValue('TOOLS_DENY')),
249
254
  },
250
255
  });
256
+ if (global.TWINCLAW_CHAT_MODE) {
257
+ startBasicREPL(gateway);
258
+ }
251
259
  const telegramBotToken = secretVault.readSecret('TELEGRAM_BOT_TOKEN') ?? getConfigValue('TELEGRAM_BOT_TOKEN');
252
260
  const twinClawConfig = await readConfig();
253
261
  const telegramUserIdRaw = getConfigValue('TELEGRAM_USER_ID');
@@ -1,5 +1,6 @@
1
1
  import { getSecretVaultService } from './secret-vault.js';
2
2
  import { getConfigValue } from '../config/json-config.js';
3
+ import { logThoughtThrottled } from '../utils/logger.js';
3
4
  const DEFAULT_OPENAI_URL = 'https://api.openai.com/v1/embeddings';
4
5
  const DEFAULT_OPENAI_MODEL = 'text-embedding-3-small';
5
6
  const DEFAULT_OLLAMA_URL = 'http://localhost:11434';
@@ -81,12 +82,14 @@ export class EmbeddingService {
81
82
  }
82
83
  return null;
83
84
  }
84
- debugLog(message) {
85
+ async debugLog(message) {
85
86
  const debugFlag = process.env.DEBUG?.trim().toLowerCase();
86
87
  const debugEnabled = Boolean(debugFlag && debugFlag !== '0' && debugFlag !== 'false');
87
88
  if (debugEnabled) {
88
89
  console.warn(message);
89
90
  }
91
+ // Always log to thought log, but throttled to prevent spam
92
+ await logThoughtThrottled('embedding_error', message, 30000); // 30s throttle
90
93
  }
91
94
  getProviderOrder() {
92
95
  const configured = (getConfigValue('EMBEDDING_PROVIDER') ?? '').toLowerCase().trim();
@@ -980,8 +980,23 @@ export class ModelRouter {
980
980
  this.metrics.lastError = scrubSensitiveText(message);
981
981
  this.metrics.lastFailureAt = new Date(this.nowFn()).toISOString();
982
982
  }
983
+ getApiKeyEnvName(provider) {
984
+ const providerKeyMap = {
985
+ 'openai': 'OPENAI_API_KEY',
986
+ 'anthropic': 'ANTHROPIC_API_KEY',
987
+ 'google': 'GEMINI_API_KEY',
988
+ 'openrouter': 'OPENROUTER_API_KEY',
989
+ 'groq': 'GROQ_API_KEY',
990
+ 'modal': 'MODAL_API_KEY',
991
+ 'copilot': 'GITHUB_TOKEN',
992
+ 'github': 'GITHUB_TOKEN',
993
+ 'ollama': 'OLLAMA_API_KEY',
994
+ };
995
+ return providerKeyMap[provider.toLowerCase()] || 'MODAL_API_KEY';
996
+ }
983
997
  loadModelsFromConfig() {
984
998
  const configModels = [];
999
+ // Check if PRIMARY_MODEL is set - use config.models.primaryModel if available
985
1000
  const primaryModelId = getConfigValue('PRIMARY_MODEL');
986
1001
  const modalApiKey = getConfigValue('MODAL_API_KEY');
987
1002
  const openRouterApiKey = getConfigValue('OPENROUTER_API_KEY');
@@ -91,8 +91,9 @@ export class ProactiveNotifier {
91
91
  return;
92
92
  if (event.type !== 'add' && event.type !== 'change')
93
93
  return;
94
- const normalizedPath = event.path.toLowerCase();
95
- const isIgnoredFile = this.#fileAlertIgnoreSubstrings.some((segment) => normalizedPath.includes(segment.toLowerCase()));
94
+ // Normalize path for robust comparison (handles Windows backslashes)
95
+ const normalizedPath = event.path.replace(/\\/g, '/').toLowerCase();
96
+ const isIgnoredFile = this.#fileAlertIgnoreSubstrings.some((segment) => normalizedPath.includes(segment.replace(/\\/g, '/').toLowerCase()));
96
97
  if (isIgnoredFile) {
97
98
  await logThought(`[ProactiveNotifier] Ignored modification event for system file: ${event.path}`);
98
99
  return;
@@ -9,34 +9,42 @@ const TASK_HINT_PATTERN = /\b(todo|task|implement|fix|build|create|refactor|ship
9
9
  const MAX_RELATION_RECONCILIATION = 12;
10
10
  const MAX_GRAPH_TRAVERSAL_DEPTH = 2;
11
11
  export async function indexConversationTurn(sessionId, role, content) {
12
- const normalized = content.trim();
13
- if (!normalized) {
14
- return;
15
- }
16
- const chunks = chunkText(normalized);
17
- let previousNodeId = null;
18
- for (const chunk of chunks) {
19
- const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
20
- const embedding = await embeddingService.embedText(taggedChunk);
21
- if (!embedding) {
22
- continue;
12
+ try {
13
+ const normalized = content.trim();
14
+ if (!normalized) {
15
+ return;
23
16
  }
24
- const memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
25
- const node = buildReasoningNode(sessionId, role, taggedChunk);
26
- upsertReasoningNode(node);
27
- linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
28
- if (previousNodeId && previousNodeId !== node.nodeId) {
29
- upsertReasoningEdge({
30
- edgeId: stableId('edge', `${previousNodeId}|${node.nodeId}|derived_from`),
31
- fromNodeId: previousNodeId,
32
- toNodeId: node.nodeId,
33
- relation: 'derived_from',
34
- weight: 0.55,
35
- provenance: `session:${sessionId}:turn-sequence`,
36
- });
17
+ const chunks = chunkText(normalized);
18
+ let previousNodeId = null;
19
+ // Limit indexing to first 2 chunks to prevent long hangs during indexing
20
+ const limitedChunks = chunks.slice(0, 2);
21
+ for (const chunk of limitedChunks) {
22
+ const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
23
+ const embedding = await embeddingService.embedText(taggedChunk);
24
+ if (!embedding) {
25
+ continue;
26
+ }
27
+ const memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
28
+ const node = buildReasoningNode(sessionId, role, taggedChunk);
29
+ upsertReasoningNode(node);
30
+ linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
31
+ if (previousNodeId && previousNodeId !== node.nodeId) {
32
+ upsertReasoningEdge({
33
+ edgeId: stableId('edge', `${previousNodeId}|${node.nodeId}|derived_from`),
34
+ fromNodeId: previousNodeId,
35
+ toNodeId: node.nodeId,
36
+ relation: 'derived_from',
37
+ weight: 0.55,
38
+ provenance: `session:${sessionId}:turn-sequence`,
39
+ });
40
+ }
41
+ previousNodeId = node.nodeId;
42
+ reconcileClaimRelations(node);
37
43
  }
38
- previousNodeId = node.nodeId;
39
- reconcileClaimRelations(node);
44
+ }
45
+ catch (err) {
46
+ const message = err instanceof Error ? err.message : String(err);
47
+ console.warn(`[SemanticMemory] Failed to index turn for session ${sessionId}: ${message}`);
40
48
  }
41
49
  }
42
50
  export async function retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK = 5) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.1.7",
3
+ "version": "1.2.0",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {