twinclaw 1.0.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/README.md +66 -0
- package/bin/npm-twinclaw.js +17 -0
- package/bin/run-twinbot-cli.js +36 -0
- package/bin/twinbot.js +4 -0
- package/bin/twinclaw.js +4 -0
- package/dist/api/handlers/browser.js +160 -0
- package/dist/api/handlers/callback.js +80 -0
- package/dist/api/handlers/config-validate.js +19 -0
- package/dist/api/handlers/health.js +117 -0
- package/dist/api/handlers/local-state-backup.js +118 -0
- package/dist/api/handlers/persona-state.js +59 -0
- package/dist/api/handlers/skill-packages.js +94 -0
- package/dist/api/router.js +278 -0
- package/dist/api/runtime-event-producer.js +99 -0
- package/dist/api/shared.js +82 -0
- package/dist/api/websocket-hub.js +305 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/env-schema.js +202 -0
- package/dist/config/env-validator.js +223 -0
- package/dist/config/identity-bootstrap.js +115 -0
- package/dist/config/json-config.js +344 -0
- package/dist/config/workspace.js +186 -0
- package/dist/core/channels-cli.js +77 -0
- package/dist/core/cli.js +119 -0
- package/dist/core/context-assembly.js +33 -0
- package/dist/core/doctor.js +365 -0
- package/dist/core/gateway-cli.js +323 -0
- package/dist/core/gateway.js +416 -0
- package/dist/core/heartbeat.js +54 -0
- package/dist/core/install-cli.js +320 -0
- package/dist/core/lane-executor.js +134 -0
- package/dist/core/logs-cli.js +70 -0
- package/dist/core/onboarding.js +760 -0
- package/dist/core/pairing-cli.js +78 -0
- package/dist/core/secret-vault-cli.js +204 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +404 -0
- package/dist/interfaces/dispatcher.js +214 -0
- package/dist/interfaces/telegram_handler.js +82 -0
- package/dist/interfaces/tui-dashboard.js +53 -0
- package/dist/interfaces/whatsapp_handler.js +94 -0
- package/dist/release/cli.js +97 -0
- package/dist/release/mvp-gate-cli.js +118 -0
- package/dist/release/twinbot-config-schema.js +162 -0
- package/dist/release/twinclaw-config-schema.js +162 -0
- package/dist/services/block-chunker.js +174 -0
- package/dist/services/browser-service.js +334 -0
- package/dist/services/context-lifecycle.js +314 -0
- package/dist/services/db.js +1055 -0
- package/dist/services/delivery-tracker.js +110 -0
- package/dist/services/dm-pairing.js +245 -0
- package/dist/services/embedding-service.js +125 -0
- package/dist/services/file-watcher.js +125 -0
- package/dist/services/inbound-debounce.js +92 -0
- package/dist/services/incident-manager.js +516 -0
- package/dist/services/job-scheduler.js +176 -0
- package/dist/services/local-state-backup.js +682 -0
- package/dist/services/mcp-client-adapter.js +291 -0
- package/dist/services/mcp-server-manager.js +143 -0
- package/dist/services/model-router.js +927 -0
- package/dist/services/mvp-gate.js +845 -0
- package/dist/services/orchestration-service.js +422 -0
- package/dist/services/persona-state.js +256 -0
- package/dist/services/policy-engine.js +92 -0
- package/dist/services/proactive-notifier.js +94 -0
- package/dist/services/queue-service.js +146 -0
- package/dist/services/release-pipeline.js +652 -0
- package/dist/services/runtime-budget-governor.js +415 -0
- package/dist/services/secret-vault.js +704 -0
- package/dist/services/semantic-memory.js +249 -0
- package/dist/services/skill-package-manager.js +806 -0
- package/dist/services/skill-registry.js +122 -0
- package/dist/services/streaming-output.js +75 -0
- package/dist/services/stt-service.js +39 -0
- package/dist/services/tts-service.js +44 -0
- package/dist/skills/builtin.js +250 -0
- package/dist/skills/shell.js +87 -0
- package/dist/skills/types.js +1 -0
- package/dist/types/api.js +1 -0
- package/dist/types/context-budget.js +1 -0
- package/dist/types/doctor.js +1 -0
- package/dist/types/file-watcher.js +1 -0
- package/dist/types/incident.js +1 -0
- package/dist/types/local-state-backup.js +1 -0
- package/dist/types/mcp.js +1 -0
- package/dist/types/messaging.js +1 -0
- package/dist/types/model-routing.js +1 -0
- package/dist/types/mvp-gate.js +2 -0
- package/dist/types/orchestration.js +1 -0
- package/dist/types/persona-state.js +22 -0
- package/dist/types/policy.js +1 -0
- package/dist/types/reasoning-graph.js +1 -0
- package/dist/types/release.js +1 -0
- package/dist/types/reliability.js +1 -0
- package/dist/types/runtime-budget.js +1 -0
- package/dist/types/scheduler.js +1 -0
- package/dist/types/secret-vault.js +1 -0
- package/dist/types/skill-packages.js +1 -0
- package/dist/types/websocket.js +14 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/secret-scan.js +208 -0
- package/mcp-servers.json +179 -0
- package/package.json +81 -0
- package/skill-packages.json +92 -0
- package/skill-packages.lock.json +5 -0
- package/src/skills/builtin.ts +275 -0
- package/src/skills/shell.ts +118 -0
- package/src/skills/types.ts +30 -0
- package/src/types/api.ts +252 -0
- package/src/types/blessed-contrib.d.ts +4 -0
- package/src/types/context-budget.ts +76 -0
- package/src/types/doctor.ts +29 -0
- package/src/types/file-watcher.ts +26 -0
- package/src/types/incident.ts +57 -0
- package/src/types/local-state-backup.ts +121 -0
- package/src/types/mcp.ts +106 -0
- package/src/types/messaging.ts +35 -0
- package/src/types/model-routing.ts +61 -0
- package/src/types/mvp-gate.ts +99 -0
- package/src/types/orchestration.ts +65 -0
- package/src/types/persona-state.ts +61 -0
- package/src/types/policy.ts +27 -0
- package/src/types/reasoning-graph.ts +58 -0
- package/src/types/release.ts +115 -0
- package/src/types/reliability.ts +43 -0
- package/src/types/runtime-budget.ts +85 -0
- package/src/types/scheduler.ts +47 -0
- package/src/types/secret-vault.ts +62 -0
- package/src/types/skill-packages.ts +81 -0
- package/src/types/sqlite-vec.d.ts +5 -0
- package/src/types/websocket.ts +122 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chunkText, EmbeddingService } from './embedding-service.js';
|
|
3
|
+
import { getMemoryProvenanceRows, getNearestMemories, getReasoningEvidenceExpansion, getReasoningNodesByClaimKey, linkMemoryProvenance, saveMemoryEmbedding, upsertReasoningEdge, upsertReasoningNode, } from './db.js';
|
|
4
|
+
const embeddingService = new EmbeddingService();
|
|
5
|
+
const NEGATION_PATTERN = /\b(no|not|never|without|cannot|can't|won't|dont|don't|didnt|didn't|isnt|isn't|arent|aren't|wasnt|wasn't|weren't)\b/i;
|
|
6
|
+
const CLAIM_KEY_STOPWORDS = /\b(a|an|the|and|or|to|of|for|in|on|at|with|is|are|was|were|be|been|being|do|does|did|this|that|it|as|by|from|no|not|never|without|cannot|cant|wont)\b/g;
|
|
7
|
+
const TASK_HINT_PATTERN = /\b(todo|task|implement|fix|build|create|refactor|ship|deploy)\b/i;
|
|
8
|
+
const MAX_RELATION_RECONCILIATION = 12;
|
|
9
|
+
const MAX_GRAPH_TRAVERSAL_DEPTH = 2;
|
|
10
|
+
export async function indexConversationTurn(sessionId, role, content) {
|
|
11
|
+
const normalized = content.trim();
|
|
12
|
+
if (!normalized) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const chunks = chunkText(normalized);
|
|
16
|
+
let previousNodeId = null;
|
|
17
|
+
for (const chunk of chunks) {
|
|
18
|
+
const taggedChunk = `${role.toUpperCase()}: ${chunk}`;
|
|
19
|
+
const embedding = await embeddingService.embedText(taggedChunk);
|
|
20
|
+
if (!embedding) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const memoryRowId = saveMemoryEmbedding(sessionId, taggedChunk, embedding);
|
|
24
|
+
const node = buildReasoningNode(sessionId, role, taggedChunk);
|
|
25
|
+
upsertReasoningNode(node);
|
|
26
|
+
linkMemoryProvenance(memoryRowId, node.nodeId, sessionId);
|
|
27
|
+
if (previousNodeId && previousNodeId !== node.nodeId) {
|
|
28
|
+
upsertReasoningEdge({
|
|
29
|
+
edgeId: stableId('edge', `${previousNodeId}|${node.nodeId}|derived_from`),
|
|
30
|
+
fromNodeId: previousNodeId,
|
|
31
|
+
toNodeId: node.nodeId,
|
|
32
|
+
relation: 'derived_from',
|
|
33
|
+
weight: 0.55,
|
|
34
|
+
provenance: `session:${sessionId}:turn-sequence`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
previousNodeId = node.nodeId;
|
|
38
|
+
reconcileClaimRelations(node);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK = 5) {
|
|
42
|
+
const embedding = await embeddingService.embedText(prompt);
|
|
43
|
+
if (!embedding) {
|
|
44
|
+
return {
|
|
45
|
+
context: '',
|
|
46
|
+
diagnostics: ['Memory retrieval skipped because prompt embedding could not be generated.'],
|
|
47
|
+
conflictCount: 0,
|
|
48
|
+
hitCount: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const candidateLimit = Math.max(topK * 3, topK);
|
|
52
|
+
const nearest = getNearestMemories(embedding, candidateLimit, sessionId);
|
|
53
|
+
if (nearest.length === 0) {
|
|
54
|
+
return {
|
|
55
|
+
context: '',
|
|
56
|
+
diagnostics: ['Memory retrieval found no nearest candidates for the active session scope.'],
|
|
57
|
+
conflictCount: 0,
|
|
58
|
+
hitCount: 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const memoryRowIds = nearest.map((item) => item.memory_rowid);
|
|
62
|
+
const provenanceRows = getMemoryProvenanceRows(memoryRowIds);
|
|
63
|
+
const provenanceByMemoryId = new Map(provenanceRows.map((row) => [row.memoryRowId, row]));
|
|
64
|
+
const seedNodeIds = Array.from(new Set(provenanceRows.map((row) => row.nodeId)));
|
|
65
|
+
const graphExpansionLimit = Math.max(12, topK * 6);
|
|
66
|
+
const expandedEdges = getReasoningEvidenceExpansion(seedNodeIds, MAX_GRAPH_TRAVERSAL_DEPTH, graphExpansionLimit);
|
|
67
|
+
const graphRelationCounts = buildGraphRelationCounts(expandedEdges);
|
|
68
|
+
const ranked = nearest
|
|
69
|
+
.map((candidate) => scoreCandidate(candidate.session_id, candidate.fact_text, candidate.memory_rowid, candidate.distance, {
|
|
70
|
+
provenance: provenanceByMemoryId.get(candidate.memory_rowid) ?? null,
|
|
71
|
+
graphRelationCounts,
|
|
72
|
+
}))
|
|
73
|
+
.sort((a, b) => b.score - a.score)
|
|
74
|
+
.slice(0, Math.max(1, topK));
|
|
75
|
+
const conflictCount = ranked.filter((candidate) => candidate.contradictionCount > 0).length;
|
|
76
|
+
const lines = ranked.map((candidate, index) => formatEvidenceLine(index, candidate));
|
|
77
|
+
const conflictNote = conflictCount > 0
|
|
78
|
+
? `\nPotential contradiction signals were detected in ${conflictCount} retrieved item(s).`
|
|
79
|
+
: '';
|
|
80
|
+
return {
|
|
81
|
+
context: `Retrieved evidence-backed memories:\n${lines.join('\n')}${conflictNote}`,
|
|
82
|
+
diagnostics: [
|
|
83
|
+
`Hybrid memory retrieval ranked ${nearest.length} vector candidates down to ${ranked.length} evidence-backed items.`,
|
|
84
|
+
`Graph traversal depth=${MAX_GRAPH_TRAVERSAL_DEPTH} collected ${expandedEdges.length} relation edge(s).`,
|
|
85
|
+
conflictCount > 0
|
|
86
|
+
? `Detected contradiction signals in ${conflictCount} candidate(s).`
|
|
87
|
+
: 'No contradiction signals detected in selected evidence set.',
|
|
88
|
+
],
|
|
89
|
+
conflictCount,
|
|
90
|
+
hitCount: ranked.length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function retrieveMemoryContext(sessionId, prompt, topK = 5) {
|
|
94
|
+
const result = await retrieveEvidenceAwareMemoryContext(sessionId, prompt, topK);
|
|
95
|
+
return result.context;
|
|
96
|
+
}
|
|
97
|
+
function buildReasoningNode(sessionId, role, content) {
|
|
98
|
+
const claimKey = normalizeClaimKey(content);
|
|
99
|
+
const polarity = detectPolarity(content);
|
|
100
|
+
return {
|
|
101
|
+
nodeId: stableId('node', `${claimKey}|${polarity}`),
|
|
102
|
+
claimKey,
|
|
103
|
+
nodeType: inferNodeType(role, content),
|
|
104
|
+
sourceRole: role.toLowerCase(),
|
|
105
|
+
canonicalText: content.trim(),
|
|
106
|
+
polarity,
|
|
107
|
+
confidence: 0.68,
|
|
108
|
+
sessionId,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function normalizeClaimKey(content) {
|
|
112
|
+
const normalized = content
|
|
113
|
+
.replace(/^[A-Z]+:\s*/, '')
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
116
|
+
.replace(/\s+/g, ' ')
|
|
117
|
+
.trim();
|
|
118
|
+
const withoutNegation = normalized
|
|
119
|
+
.replace(/\b(no|not|never|without|cannot|cant|wont|dont|didnt|isnt|arent|wasnt|werent)\b/g, ' ')
|
|
120
|
+
.replace(CLAIM_KEY_STOPWORDS, ' ')
|
|
121
|
+
.replace(/\s+/g, ' ')
|
|
122
|
+
.trim();
|
|
123
|
+
const claim = withoutNegation || normalized;
|
|
124
|
+
return claim.slice(0, 180).trim();
|
|
125
|
+
}
|
|
126
|
+
function inferNodeType(role, content) {
|
|
127
|
+
if (role.toLowerCase() === 'tool') {
|
|
128
|
+
return 'artifact';
|
|
129
|
+
}
|
|
130
|
+
if (TASK_HINT_PATTERN.test(content)) {
|
|
131
|
+
return 'task';
|
|
132
|
+
}
|
|
133
|
+
return 'fact';
|
|
134
|
+
}
|
|
135
|
+
function detectPolarity(content) {
|
|
136
|
+
return NEGATION_PATTERN.test(content) ? -1 : 1;
|
|
137
|
+
}
|
|
138
|
+
function stableId(prefix, seed) {
|
|
139
|
+
const digest = createHash('sha1').update(seed).digest('hex').slice(0, 20);
|
|
140
|
+
return `${prefix}:${digest}`;
|
|
141
|
+
}
|
|
142
|
+
function reconcileClaimRelations(node) {
|
|
143
|
+
const related = getReasoningNodesByClaimKey(node.claimKey)
|
|
144
|
+
.filter((item) => item.node_id !== node.nodeId)
|
|
145
|
+
.slice(0, MAX_RELATION_RECONCILIATION);
|
|
146
|
+
for (const item of related) {
|
|
147
|
+
const relation = item.polarity === node.polarity ? 'supports' : 'contradicts';
|
|
148
|
+
const provenance = `claim:${node.claimKey.slice(0, 40)}`;
|
|
149
|
+
upsertReasoningEdge({
|
|
150
|
+
edgeId: stableId('edge', `${item.node_id}|${node.nodeId}|${relation}`),
|
|
151
|
+
fromNodeId: item.node_id,
|
|
152
|
+
toNodeId: node.nodeId,
|
|
153
|
+
relation,
|
|
154
|
+
weight: relation === 'supports' ? 0.72 : 0.9,
|
|
155
|
+
provenance,
|
|
156
|
+
});
|
|
157
|
+
if (relation === 'supports' || relation === 'contradicts') {
|
|
158
|
+
upsertReasoningEdge({
|
|
159
|
+
edgeId: stableId('edge', `${node.nodeId}|${item.node_id}|${relation}`),
|
|
160
|
+
fromNodeId: node.nodeId,
|
|
161
|
+
toNodeId: item.node_id,
|
|
162
|
+
relation,
|
|
163
|
+
weight: relation === 'supports' ? 0.72 : 0.9,
|
|
164
|
+
provenance,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function buildGraphRelationCounts(edges) {
|
|
170
|
+
const counts = new Map();
|
|
171
|
+
const increment = (nodeId, relation) => {
|
|
172
|
+
const current = counts.get(nodeId) ?? { supports: 0, contradicts: 0, depends: 0, derived: 0 };
|
|
173
|
+
if (relation === 'supports') {
|
|
174
|
+
current.supports += 1;
|
|
175
|
+
}
|
|
176
|
+
else if (relation === 'contradicts') {
|
|
177
|
+
current.contradicts += 1;
|
|
178
|
+
}
|
|
179
|
+
else if (relation === 'depends_on') {
|
|
180
|
+
current.depends += 1;
|
|
181
|
+
}
|
|
182
|
+
else if (relation === 'derived_from') {
|
|
183
|
+
current.derived += 1;
|
|
184
|
+
}
|
|
185
|
+
counts.set(nodeId, current);
|
|
186
|
+
};
|
|
187
|
+
for (const edge of edges) {
|
|
188
|
+
increment(edge.fromNodeId, edge.relation);
|
|
189
|
+
increment(edge.toNodeId, edge.relation);
|
|
190
|
+
}
|
|
191
|
+
return counts;
|
|
192
|
+
}
|
|
193
|
+
function scoreCandidate(sessionId, factText, memoryRowId, distance, options) {
|
|
194
|
+
const provenance = options.provenance;
|
|
195
|
+
const vectorScore = 1 / (1 + Math.max(0, distance));
|
|
196
|
+
const graphCounts = provenance
|
|
197
|
+
? options.graphRelationCounts.get(provenance.nodeId) ?? {
|
|
198
|
+
supports: 0,
|
|
199
|
+
contradicts: 0,
|
|
200
|
+
depends: 0,
|
|
201
|
+
derived: 0,
|
|
202
|
+
}
|
|
203
|
+
: { supports: 0, contradicts: 0, depends: 0, derived: 0 };
|
|
204
|
+
const supports = (provenance?.supportsCount ?? 0) + graphCounts.supports;
|
|
205
|
+
const contradicts = (provenance?.contradictsCount ?? 0) + graphCounts.contradicts;
|
|
206
|
+
const depends = (provenance?.dependsCount ?? 0) + graphCounts.depends;
|
|
207
|
+
const derived = (provenance?.derivedCount ?? 0) + graphCounts.derived;
|
|
208
|
+
const relationScore = Math.min(1, supports * 0.2 + depends * 0.11 + derived * 0.08);
|
|
209
|
+
const recencyScore = estimateRecencyScore(provenance?.updatedAt);
|
|
210
|
+
const contradictionPenalty = Math.min(0.35, contradicts * 0.08);
|
|
211
|
+
const score = vectorScore * 0.62 + relationScore * 0.26 + recencyScore * 0.12 - contradictionPenalty;
|
|
212
|
+
return {
|
|
213
|
+
sessionId,
|
|
214
|
+
factText,
|
|
215
|
+
memoryRowId,
|
|
216
|
+
score,
|
|
217
|
+
provenance,
|
|
218
|
+
contradictionCount: contradicts,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function estimateRecencyScore(updatedAt) {
|
|
222
|
+
if (!updatedAt) {
|
|
223
|
+
return 0.5;
|
|
224
|
+
}
|
|
225
|
+
const timestamp = Date.parse(updatedAt.replace(' ', 'T'));
|
|
226
|
+
if (!Number.isFinite(timestamp)) {
|
|
227
|
+
return 0.5;
|
|
228
|
+
}
|
|
229
|
+
const ageHours = Math.max(0, (Date.now() - timestamp) / (1000 * 60 * 60));
|
|
230
|
+
if (ageHours <= 1) {
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
if (ageHours <= 24) {
|
|
234
|
+
return 0.82;
|
|
235
|
+
}
|
|
236
|
+
if (ageHours <= 24 * 7) {
|
|
237
|
+
return 0.67;
|
|
238
|
+
}
|
|
239
|
+
return 0.48;
|
|
240
|
+
}
|
|
241
|
+
function formatEvidenceLine(index, candidate) {
|
|
242
|
+
const snippet = candidate.factText.length > 220
|
|
243
|
+
? `${candidate.factText.slice(0, 217)}...`
|
|
244
|
+
: candidate.factText;
|
|
245
|
+
const nodeRef = candidate.provenance?.nodeId.slice(0, 18) ?? 'unlinked';
|
|
246
|
+
const claimRef = candidate.provenance?.claimKey.slice(0, 28) ?? 'unlinked';
|
|
247
|
+
return (`${index + 1}. (${candidate.sessionId}) ${snippet}\n` +
|
|
248
|
+
` provenance=[memory:${candidate.memoryRowId} node:${nodeRef} claim:${claimRef}] score=${candidate.score.toFixed(3)}`);
|
|
249
|
+
}
|