titan-agent 7.1.0 → 7.2.1
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 +31 -0
- package/dist/agent/agent.js +25 -5
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/disciplinePrompt.js +41 -0
- package/dist/agent/disciplinePrompt.js.map +1 -0
- package/dist/agent/honestyGuard.js +66 -0
- package/dist/agent/honestyGuard.js.map +1 -0
- package/dist/agent/selfCritique.js +101 -0
- package/dist/agent/selfCritique.js.map +1 -0
- package/dist/agent/trajectoryLogger.js +19 -2
- package/dist/agent/trajectoryLogger.js.map +1 -1
- package/dist/analytics/collector.js +1 -1
- package/dist/analytics/collector.js.map +1 -1
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/server.js +1 -1
- package/dist/gateway/server.js.map +1 -1
- package/dist/providers/router.js +1 -1
- package/dist/providers/router.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
function looksComplex(message) {
|
|
3
|
+
const m = (message || "").trim();
|
|
4
|
+
if (m.length < 40) return false;
|
|
5
|
+
let signals = 0;
|
|
6
|
+
if (/\b(then|after that|next|finally|first|second|step \d|once .* (is )?done)\b/i.test(m)) signals++;
|
|
7
|
+
if (/\band\b.*\band\b/i.test(m)) signals++;
|
|
8
|
+
if (/\b(\d+)\s+(files?|steps?|things?|parts?|tasks?)\b/i.test(m)) signals++;
|
|
9
|
+
const listItems = (m.match(/(^|\n)\s*(\d+[.)]|[-*])\s+\S/gm) || []).length;
|
|
10
|
+
if (listItems >= 2) signals += 2;
|
|
11
|
+
else if (listItems === 1) signals++;
|
|
12
|
+
const actionVerbs = (m.match(/\b(refactor|migrate|implement|build|integrate|wire\s?up|set\s?up|deploy|redesign|add|update|create|write|fix|test|configure|install|remove)\b/gi) || []).length;
|
|
13
|
+
if (actionVerbs >= 3) signals += 2;
|
|
14
|
+
else if (actionVerbs >= 1) signals++;
|
|
15
|
+
if (m.length > 400) signals++;
|
|
16
|
+
return signals >= 2;
|
|
17
|
+
}
|
|
18
|
+
const PLAN_FIRST = [
|
|
19
|
+
"DISCIPLINE \u2014 plan first: this task looks multi-step. Before calling tools,",
|
|
20
|
+
"write a short numbered plan (the steps and the order). Then execute it,",
|
|
21
|
+
"verifying each step before moving on. Do not claim a step is done until you",
|
|
22
|
+
"have checked it. A plan you followed beats a fast answer you have to redo."
|
|
23
|
+
].join(" ");
|
|
24
|
+
const ESCALATE = [
|
|
25
|
+
"DISCIPLINE \u2014 escalate the irreversible: before any action that is",
|
|
26
|
+
"destructive (deletes/overwrites data), public (sends/posts/publishes),",
|
|
27
|
+
"spends money, or changes a shared service, STOP and confirm with the user",
|
|
28
|
+
"first unless they already authorized THIS action. Reversible, local work:",
|
|
29
|
+
"proceed. When unsure which, prefer the reversible reading and say so."
|
|
30
|
+
].join(" ");
|
|
31
|
+
function buildDisciplinePrompt(message, reliabilityMode) {
|
|
32
|
+
if (!reliabilityMode) return "";
|
|
33
|
+
const parts = [ESCALATE];
|
|
34
|
+
if (looksComplex(message)) parts.unshift(PLAN_FIRST);
|
|
35
|
+
return "\n\n" + parts.join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
buildDisciplinePrompt,
|
|
39
|
+
looksComplex
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=disciplinePrompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/disciplinePrompt.ts"],"sourcesContent":["/**\n * Discipline Prompt — the plan-first + escalation habits (v7.2.x).\n *\n * Organs 4 (decompose) and 6 (judgment) of the Fable-5 rebuild plan, delivered\n * as a first-turn system-prompt fragment gated by Reliability Mode. A frontier\n * model plans hard tasks and escalates risky ones by reflex; a weaker local\n * model must be TOLD to. This makes the reflex explicit — so under Reliability\n * Mode a local brain behaves with the same \"plan before you build, ask before\n * you do something irreversible\" discipline that makes a strong model feel\n * dependable.\n *\n * Pure functions → unit-testable. Fires ONLY when reliabilityMode is on, and\n * the plan-first half only for tasks that look genuinely multi-step (a cheap\n * heuristic — over-injecting on trivial turns would just add noise).\n */\n\n/** Heuristic: does this task look complex enough to warrant planning first? Pure. */\nexport function looksComplex(message: string): boolean {\n const m = (message || '').trim();\n if (m.length < 40) return false;\n let signals = 0;\n // Sequencing language.\n if (/\\b(then|after that|next|finally|first|second|step \\d|once .* (is )?done)\\b/i.test(m)) signals++;\n // Explicit multiplicity.\n if (/\\band\\b.*\\band\\b/i.test(m)) signals++;\n if (/\\b(\\d+)\\s+(files?|steps?|things?|parts?|tasks?)\\b/i.test(m)) signals++;\n // A list of asks IS a decomposable task: 2+ list items → strongly complex.\n const listItems = (m.match(/(^|\\n)\\s*(\\d+[.)]|[-*])\\s+\\S/gm) || []).length;\n if (listItems >= 2) signals += 2;\n else if (listItems === 1) signals++;\n // Action verbs that usually span files/steps. COUNT them — a task asking\n // for several distinct actions (\"build X, integrate Y, and deploy Z\") is\n // decomposable even without sequencing words.\n const actionVerbs = (m.match(/\\b(refactor|migrate|implement|build|integrate|wire\\s?up|set\\s?up|deploy|redesign|add|update|create|write|fix|test|configure|install|remove)\\b/gi) || []).length;\n if (actionVerbs >= 3) signals += 2;\n else if (actionVerbs >= 1) signals++;\n // Sheer length is itself a weak signal.\n if (m.length > 400) signals++;\n return signals >= 2;\n}\n\nconst PLAN_FIRST = [\n 'DISCIPLINE — plan first: this task looks multi-step. Before calling tools,',\n 'write a short numbered plan (the steps and the order). Then execute it,',\n 'verifying each step before moving on. Do not claim a step is done until you',\n 'have checked it. A plan you followed beats a fast answer you have to redo.',\n].join(' ');\n\nconst ESCALATE = [\n 'DISCIPLINE — escalate the irreversible: before any action that is',\n 'destructive (deletes/overwrites data), public (sends/posts/publishes),',\n 'spends money, or changes a shared service, STOP and confirm with the user',\n 'first unless they already authorized THIS action. Reversible, local work:',\n 'proceed. When unsure which, prefer the reversible reading and say so.',\n].join(' ');\n\n/**\n * Build the discipline fragment to append to the first-turn system prompt.\n * Returns '' when reliabilityMode is off. Pure.\n */\nexport function buildDisciplinePrompt(message: string, reliabilityMode: boolean): string {\n if (!reliabilityMode) return '';\n const parts = [ESCALATE]; // escalation discipline always applies under Reliability Mode\n if (looksComplex(message)) parts.unshift(PLAN_FIRST);\n return '\\n\\n' + parts.join('\\n\\n');\n}\n"],"mappings":";AAiBO,SAAS,aAAa,SAA0B;AACnD,QAAM,KAAK,WAAW,IAAI,KAAK;AAC/B,MAAI,EAAE,SAAS,GAAI,QAAO;AAC1B,MAAI,UAAU;AAEd,MAAI,8EAA8E,KAAK,CAAC,EAAG;AAE3F,MAAI,oBAAoB,KAAK,CAAC,EAAG;AACjC,MAAI,qDAAqD,KAAK,CAAC,EAAG;AAElE,QAAM,aAAa,EAAE,MAAM,gCAAgC,KAAK,CAAC,GAAG;AACpE,MAAI,aAAa,EAAG,YAAW;AAAA,WACtB,cAAc,EAAG;AAI1B,QAAM,eAAe,EAAE,MAAM,iJAAiJ,KAAK,CAAC,GAAG;AACvL,MAAI,eAAe,EAAG,YAAW;AAAA,WACxB,eAAe,EAAG;AAE3B,MAAI,EAAE,SAAS,IAAK;AACpB,SAAO,WAAW;AACtB;AAEA,MAAM,aAAa;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,EAAE,KAAK,GAAG;AAEV,MAAM,WAAW;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,EAAE,KAAK,GAAG;AAMH,SAAS,sBAAsB,SAAiB,iBAAkC;AACrF,MAAI,CAAC,gBAAiB,QAAO;AAC7B,QAAM,QAAQ,CAAC,QAAQ;AACvB,MAAI,aAAa,OAAO,EAAG,OAAM,QAAQ,UAAU;AACnD,SAAO,SAAS,MAAM,KAAK,MAAM;AACrC;","names":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const ACTION_CLAIM_RULES = [
|
|
3
|
+
{
|
|
4
|
+
action: "scheduling",
|
|
5
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:set|scheduled|created)|(?:reminder|schedule[d]?)\s+(?:is\s+)?(?:set|created|scheduled)|i(?:'|’)?ll remind you|scheduled a reminder)\b/i,
|
|
6
|
+
satisfiedBy: /(^|_)(reminder|cron|schedule|event_trigger|workflows?|social_scheduler)/i,
|
|
7
|
+
correction: "I described scheduling that, but I did not actually create a scheduled job this turn. Ask me again and I'll set it with the reminder tool properly."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
action: "sending a message/email",
|
|
11
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:sent|emailed|messaged|replied|forwarded)|(?:email|message|reply)\s+(?:has been|was)\s+sent|i(?:'|’)?ve dm(?:'|’)?d)\b/i,
|
|
12
|
+
satisfiedBy: /(^|_)(send|email|message|reply|forward|dm|sms|slack|discord|telegram|whatsapp|matrix|notify|post_message|messenger)/i,
|
|
13
|
+
correction: "I described sending that message, but no send tool ran this turn \u2014 nothing was actually sent. Tell me to send it and I will."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
action: "posting/publishing",
|
|
17
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:posted|published|tweeted|shared)|(?:posted|published)\s+(?:it|the|to)\b)/i,
|
|
18
|
+
satisfiedBy: /(^|_)(post|publish|tweet|share|x_|fb_|social|content_publish|deploy_website)/i,
|
|
19
|
+
correction: "I described posting/publishing that, but no publish tool ran this turn \u2014 nothing went live. Confirm and I'll actually publish it."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
action: "deleting/removing",
|
|
23
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:deleted|removed|cleared|wiped)|(?:has been|was)\s+(?:deleted|removed))\b/i,
|
|
24
|
+
satisfiedBy: /(^|_)(delete|remove|clear|wipe|purge|rm|trash|unlink)/i,
|
|
25
|
+
correction: "I described deleting that, but no delete tool ran this turn \u2014 nothing was actually removed."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
action: "deploying",
|
|
29
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:deployed|shipped|released|pushed live)|(?:has been|was)\s+deployed|deployment\s+(?:is\s+)?complete)\b/i,
|
|
30
|
+
satisfiedBy: /(^|_)(deploy|ship|release|publish|rollout|kubectl|docker_push)/i,
|
|
31
|
+
correction: "I described deploying that, but no deploy tool ran this turn \u2014 nothing was actually deployed."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
action: "writing a file",
|
|
35
|
+
claim: /\b(i(?:'|’)?(?:ve| have)\s+(?:saved|written|created|updated)\s+(?:the\s+)?(?:file|it to|config)|(?:file|it)\s+(?:has been|was)\s+(?:saved|written|created))\b/i,
|
|
36
|
+
satisfiedBy: /(^|_)(write|save|edit|create_file|file_write|append|patch|str_replace)/i,
|
|
37
|
+
correction: "I described saving/writing that file, but no file-write tool ran this turn \u2014 nothing was actually written to disk."
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function detectFabricatedActions(finalContent, toolsUsed) {
|
|
41
|
+
if (!finalContent) return [];
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const rule of ACTION_CLAIM_RULES) {
|
|
44
|
+
if (!rule.claim.test(finalContent)) continue;
|
|
45
|
+
const satisfied = toolsUsed.some((t) => rule.satisfiedBy.test(t));
|
|
46
|
+
if (!satisfied) findings.push({ action: rule.action, correction: rule.correction });
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
50
|
+
function applyHonestyGuard(finalContent, toolsUsed) {
|
|
51
|
+
const findings = detectFabricatedActions(finalContent, toolsUsed);
|
|
52
|
+
if (findings.length === 0) return { content: finalContent, flagged: [] };
|
|
53
|
+
const block = findings.length === 1 ? `
|
|
54
|
+
|
|
55
|
+
\u26A0\uFE0F Correction: ${findings[0].correction}` : `
|
|
56
|
+
|
|
57
|
+
\u26A0\uFE0F Correction: I described actions I didn't actually take this turn:
|
|
58
|
+
${findings.map((f) => `\u2022 ${f.correction}`).join("\n")}`;
|
|
59
|
+
return { content: finalContent + block, flagged: findings.map((f) => f.action) };
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
ACTION_CLAIM_RULES,
|
|
63
|
+
applyHonestyGuard,
|
|
64
|
+
detectFabricatedActions
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=honestyGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/honestyGuard.ts"],"sourcesContent":["/**\n * Honesty Guard — the verification wall (v7.1.x).\n *\n * The deterministic backstop that makes TITAN structurally honest: if a reply\n * CLAIMS it performed a side-effectful action (sent, posted, deleted, deployed,\n * saved a file, scheduled a job…) but no tool capable of that action ran this\n * turn, the claim is fabricated and gets a visible correction appended.\n *\n * Model-agnostic and deterministic by design — prompt steering tries to\n * prevent fabrication, but a weaker local model will still bluff; this\n * guarantees the user is never silently misled. It is the single most\n * important organ for running TITAN \"like Fable 5\" on any model: honesty is\n * not a capability, it's an enforced invariant.\n *\n * Pure function → fully unit-testable. Only APPENDS text; never blocks tools\n * or changes behavior, so its blast radius is a string concat.\n */\n\nexport interface ActionClaimRule {\n /** Short name of the action class (for the correction message + logs). */\n action: string;\n /** Reply-text pattern that indicates the model CLAIMED this action. */\n claim: RegExp;\n /** A tool name matching this satisfies the claim (segment-aware). */\n satisfiedBy: RegExp;\n /** What to tell the user when the claim is unbacked. */\n correction: string;\n}\n\n/**\n * Rules map \"the reply says it did X\" → \"a tool that does X must have run\".\n * Kept conservative: each `claim` targets explicit past/committed phrasing\n * (\"I've sent\", \"posted the\", \"deleted the\") — NOT hedged/intent phrasing\n * (\"I can send\", \"want me to post\") — so we never false-flag an offer.\n */\nexport const ACTION_CLAIM_RULES: ActionClaimRule[] = [\n {\n action: 'scheduling',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:set|scheduled|created)|(?:reminder|schedule[d]?)\\s+(?:is\\s+)?(?:set|created|scheduled)|i(?:'|’)?ll remind you|scheduled a reminder)\\b/i,\n satisfiedBy: /(^|_)(reminder|cron|schedule|event_trigger|workflows?|social_scheduler)/i,\n correction: 'I described scheduling that, but I did not actually create a scheduled job this turn. Ask me again and I\\'ll set it with the reminder tool properly.',\n },\n {\n action: 'sending a message/email',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:sent|emailed|messaged|replied|forwarded)|(?:email|message|reply)\\s+(?:has been|was)\\s+sent|i(?:'|’)?ve dm(?:'|’)?d)\\b/i,\n satisfiedBy: /(^|_)(send|email|message|reply|forward|dm|sms|slack|discord|telegram|whatsapp|matrix|notify|post_message|messenger)/i,\n correction: 'I described sending that message, but no send tool ran this turn — nothing was actually sent. Tell me to send it and I will.',\n },\n {\n action: 'posting/publishing',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:posted|published|tweeted|shared)|(?:posted|published)\\s+(?:it|the|to)\\b)/i,\n satisfiedBy: /(^|_)(post|publish|tweet|share|x_|fb_|social|content_publish|deploy_website)/i,\n correction: 'I described posting/publishing that, but no publish tool ran this turn — nothing went live. Confirm and I\\'ll actually publish it.',\n },\n {\n action: 'deleting/removing',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:deleted|removed|cleared|wiped)|(?:has been|was)\\s+(?:deleted|removed))\\b/i,\n satisfiedBy: /(^|_)(delete|remove|clear|wipe|purge|rm|trash|unlink)/i,\n correction: 'I described deleting that, but no delete tool ran this turn — nothing was actually removed.',\n },\n {\n action: 'deploying',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:deployed|shipped|released|pushed live)|(?:has been|was)\\s+deployed|deployment\\s+(?:is\\s+)?complete)\\b/i,\n satisfiedBy: /(^|_)(deploy|ship|release|publish|rollout|kubectl|docker_push)/i,\n correction: 'I described deploying that, but no deploy tool ran this turn — nothing was actually deployed.',\n },\n {\n action: 'writing a file',\n claim: /\\b(i(?:'|’)?(?:ve| have)\\s+(?:saved|written|created|updated)\\s+(?:the\\s+)?(?:file|it to|config)|(?:file|it)\\s+(?:has been|was)\\s+(?:saved|written|created))\\b/i,\n satisfiedBy: /(^|_)(write|save|edit|create_file|file_write|append|patch|str_replace)/i,\n correction: 'I described saving/writing that file, but no file-write tool ran this turn — nothing was actually written to disk.',\n },\n];\n\nexport interface HonestyFinding {\n action: string;\n correction: string;\n}\n\n/**\n * Detect fabricated action-claims. Pure: returns the unbacked claims, in rule\n * order, deduped by action. `toolsUsed` is the list of tool names that ran.\n */\nexport function detectFabricatedActions(finalContent: string, toolsUsed: string[]): HonestyFinding[] {\n if (!finalContent) return [];\n const findings: HonestyFinding[] = [];\n for (const rule of ACTION_CLAIM_RULES) {\n if (!rule.claim.test(finalContent)) continue;\n const satisfied = toolsUsed.some(t => rule.satisfiedBy.test(t));\n if (!satisfied) findings.push({ action: rule.action, correction: rule.correction });\n }\n return findings;\n}\n\n/**\n * Apply the guard: returns the (possibly corrected) content + which actions\n * were flagged. Appends ONE correction block for all findings.\n */\nexport function applyHonestyGuard(finalContent: string, toolsUsed: string[]): { content: string; flagged: string[] } {\n const findings = detectFabricatedActions(finalContent, toolsUsed);\n if (findings.length === 0) return { content: finalContent, flagged: [] };\n const block = findings.length === 1\n ? `\\n\\n⚠️ Correction: ${findings[0].correction}`\n : `\\n\\n⚠️ Correction: I described actions I didn't actually take this turn:\\n${findings.map(f => `• ${f.correction}`).join('\\n')}`;\n return { content: finalContent + block, flagged: findings.map(f => f.action) };\n}\n"],"mappings":";AAmCO,MAAM,qBAAwC;AAAA,EACjD;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,IACI,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,aAAa;AAAA,IACb,YAAY;AAAA,EAChB;AACJ;AAWO,SAAS,wBAAwB,cAAsB,WAAuC;AACjG,MAAI,CAAC,aAAc,QAAO,CAAC;AAC3B,QAAM,WAA6B,CAAC;AACpC,aAAW,QAAQ,oBAAoB;AACnC,QAAI,CAAC,KAAK,MAAM,KAAK,YAAY,EAAG;AACpC,UAAM,YAAY,UAAU,KAAK,OAAK,KAAK,YAAY,KAAK,CAAC,CAAC;AAC9D,QAAI,CAAC,UAAW,UAAS,KAAK,EAAE,QAAQ,KAAK,QAAQ,YAAY,KAAK,WAAW,CAAC;AAAA,EACtF;AACA,SAAO;AACX;AAMO,SAAS,kBAAkB,cAAsB,WAA6D;AACjH,QAAM,WAAW,wBAAwB,cAAc,SAAS;AAChE,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,SAAS,cAAc,SAAS,CAAC,EAAE;AACvE,QAAM,QAAQ,SAAS,WAAW,IAC5B;AAAA;AAAA,2BAAsB,SAAS,CAAC,EAAE,UAAU,KAC5C;AAAA;AAAA;AAAA,EAA6E,SAAS,IAAI,OAAK,UAAK,EAAE,UAAU,EAAE,EAAE,KAAK,IAAI,CAAC;AACpI,SAAO,EAAE,SAAS,eAAe,OAAO,SAAS,SAAS,IAAI,OAAK,EAAE,MAAM,EAAE;AACjF;","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import logger from "../utils/logger.js";
|
|
3
|
+
const COMPONENT = "SelfCritique";
|
|
4
|
+
const DRAFT_CAP = 4e3;
|
|
5
|
+
const CRITIQUE_MAX_TOKENS = 400;
|
|
6
|
+
const CRITIQUE_TIMEOUT_MS = 3e4;
|
|
7
|
+
const DEFAULT_SELF_CRITIQUE = {
|
|
8
|
+
enabled: false,
|
|
9
|
+
minToolCalls: 1,
|
|
10
|
+
minDraftChars: 200,
|
|
11
|
+
model: ""
|
|
12
|
+
};
|
|
13
|
+
function resolveSelfCritique(selfCritique, reliabilityMode) {
|
|
14
|
+
const base = { ...DEFAULT_SELF_CRITIQUE, ...selfCritique || {} };
|
|
15
|
+
if (reliabilityMode && selfCritique?.enabled === void 0) base.enabled = true;
|
|
16
|
+
return base;
|
|
17
|
+
}
|
|
18
|
+
function shouldCritique(cfg, draft, toolsUsed) {
|
|
19
|
+
if (!cfg.enabled) return false;
|
|
20
|
+
if (!draft || draft.length < cfg.minDraftChars) return false;
|
|
21
|
+
if (toolsUsed.length < cfg.minToolCalls) return false;
|
|
22
|
+
if (/On reflection[:,]|⚠️ Correction:/.test(draft)) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
function buildCritiquePrompt(task, draft, toolsUsed) {
|
|
26
|
+
return [
|
|
27
|
+
"You are reviewing your OWN draft answer adversarially, before it reaches the user.",
|
|
28
|
+
"Be skeptical: your job is to catch what the draft got wrong, not to praise it.",
|
|
29
|
+
"",
|
|
30
|
+
`USER TASK: ${task.slice(0, 800)}`,
|
|
31
|
+
`TOOLS YOU ACTUALLY RAN THIS TURN: ${toolsUsed.length ? toolsUsed.join(", ") : "(none)"}`,
|
|
32
|
+
"",
|
|
33
|
+
"YOUR DRAFT ANSWER:",
|
|
34
|
+
'"""',
|
|
35
|
+
draft.slice(0, DRAFT_CAP),
|
|
36
|
+
'"""',
|
|
37
|
+
"",
|
|
38
|
+
"Find ONLY concrete problems in these three classes:",
|
|
39
|
+
" 1. CLAIMED-BUT-UNVERIFIED: the draft states something happened or is true that the tools run above do NOT actually support.",
|
|
40
|
+
" 2. LIKELY-WRONG: a factual/logical error a careful reader would catch.",
|
|
41
|
+
" 3. UNSTATED-RISK: a caveat or failure case the user needs but the draft omits.",
|
|
42
|
+
"",
|
|
43
|
+
"If the draft is solid, reply with exactly: SOLID",
|
|
44
|
+
'Otherwise reply with 1-3 short bullet lines, each starting with "- ", naming a real issue. No preamble.'
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function parseCritique(response) {
|
|
48
|
+
const text = (response || "").trim();
|
|
49
|
+
if (!text || /^solid\b/i.test(text)) return { solid: true, issues: [] };
|
|
50
|
+
const issues = text.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("-")).map((l) => l.replace(/^-\s*/, "").trim()).filter(Boolean).slice(0, 3);
|
|
51
|
+
if (issues.length === 0) return { solid: true, issues: [] };
|
|
52
|
+
return { solid: false, issues };
|
|
53
|
+
}
|
|
54
|
+
function formatReflection(issues) {
|
|
55
|
+
if (issues.length === 0) return "";
|
|
56
|
+
if (issues.length === 1) return `
|
|
57
|
+
|
|
58
|
+
_On reflection: ${issues[0]}_`;
|
|
59
|
+
return `
|
|
60
|
+
|
|
61
|
+
_On reflection, a couple of caveats:_
|
|
62
|
+
${issues.map((i) => `- ${i}`).join("\n")}`;
|
|
63
|
+
}
|
|
64
|
+
async function runSelfCritique(cfg, task, draft, toolsUsed, turnModel) {
|
|
65
|
+
if (!shouldCritique(cfg, draft, toolsUsed)) return { content: draft, critiqued: false, issues: [] };
|
|
66
|
+
try {
|
|
67
|
+
const { chat } = await import("../providers/router.js");
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timer = setTimeout(() => controller.abort(), CRITIQUE_TIMEOUT_MS);
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
const res = await chat({
|
|
73
|
+
model: cfg.model || turnModel,
|
|
74
|
+
messages: [{ role: "user", content: buildCritiquePrompt(task, draft, toolsUsed) }],
|
|
75
|
+
maxTokens: CRITIQUE_MAX_TOKENS,
|
|
76
|
+
temperature: 0.2,
|
|
77
|
+
signal: controller.signal
|
|
78
|
+
});
|
|
79
|
+
response = res.content || "";
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
const verdict = parseCritique(response);
|
|
84
|
+
if (verdict.solid) return { content: draft, critiqued: true, issues: [] };
|
|
85
|
+
logger.info(COMPONENT, `Self-critique surfaced ${verdict.issues.length} issue(s); appended reflection`);
|
|
86
|
+
return { content: draft + formatReflection(verdict.issues), critiqued: true, issues: verdict.issues };
|
|
87
|
+
} catch (e) {
|
|
88
|
+
logger.debug(COMPONENT, `Self-critique skipped (error): ${e.message}`);
|
|
89
|
+
return { content: draft, critiqued: false, issues: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
DEFAULT_SELF_CRITIQUE,
|
|
94
|
+
buildCritiquePrompt,
|
|
95
|
+
formatReflection,
|
|
96
|
+
parseCritique,
|
|
97
|
+
resolveSelfCritique,
|
|
98
|
+
runSelfCritique,
|
|
99
|
+
shouldCritique
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=selfCritique.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/selfCritique.ts"],"sourcesContent":["/**\n * Self-Critique — adversarial reflection pass (v7.1.x).\n *\n * Organ 5 of the Fable-5 rebuild plan (docs/handoff/REBUILD-FABLE-5.md): the\n * discipline that lets a capable local model reach frontier-*reliability*\n * without frontier-*IQ*. A single model can't out-think itself in one pass —\n * but it can catch its own mistakes in a SECOND pass framed adversarially.\n *\n * After a substantive turn, one bounded critique call asks the model to find,\n * in its own draft: claims it made but did NOT verify, likely errors, and\n * unstated risks. If the draft is solid → no change. If not → an honest\n * \"On reflection\" caveat is APPENDED (never a silent rewrite — revising can\n * make things worse; a caveat is strictly informative and safe).\n *\n * OFF BY DEFAULT (`agent.selfCritique.enabled`). Single extra call, hard\n * length caps, its own timeout — bounded latency/cost. Skips trivial turns.\n * Pure helpers (gate, prompt, parse) are exported for unit tests.\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'SelfCritique';\nconst DRAFT_CAP = 4_000;\nconst CRITIQUE_MAX_TOKENS = 400;\nconst CRITIQUE_TIMEOUT_MS = 30_000;\n\nexport interface SelfCritiqueConfig {\n enabled: boolean;\n /** Only critique turns that ran at least this many tools (substance gate). */\n minToolCalls: number;\n /** Skip drafts shorter than this (trivial answers). */\n minDraftChars: number;\n /** Model for the critique pass; empty = same as the turn's model. */\n model: string;\n}\n\nexport const DEFAULT_SELF_CRITIQUE: SelfCritiqueConfig = {\n enabled: false,\n minToolCalls: 1,\n minDraftChars: 200,\n model: '',\n};\n\n/**\n * Resolve the effective self-critique config given the one-switch\n * `reliabilityMode`. Reliability Mode is the \"best harness\" preset: it turns\n * on self-critique (Organ 5) without the user editing nested config. An\n * explicit `selfCritique.enabled` still wins if set. Pure.\n */\nexport function resolveSelfCritique(\n selfCritique: Partial<SelfCritiqueConfig> | undefined,\n reliabilityMode: boolean,\n): SelfCritiqueConfig {\n const base = { ...DEFAULT_SELF_CRITIQUE, ...(selfCritique || {}) };\n // reliabilityMode flips the default ON; an explicit false in config still wins.\n if (reliabilityMode && selfCritique?.enabled === undefined) base.enabled = true;\n return base;\n}\n\n/** Should this turn be critiqued? Pure. */\nexport function shouldCritique(cfg: SelfCritiqueConfig, draft: string, toolsUsed: string[]): boolean {\n if (!cfg.enabled) return false;\n if (!draft || draft.length < cfg.minDraftChars) return false;\n if (toolsUsed.length < cfg.minToolCalls) return false;\n // Never critique a reply that's already a correction/caveat (avoid loops).\n if (/On reflection[:,]|⚠️ Correction:/.test(draft)) return false;\n return true;\n}\n\n/** Build the adversarial critique prompt. Pure. */\nexport function buildCritiquePrompt(task: string, draft: string, toolsUsed: string[]): string {\n return [\n 'You are reviewing your OWN draft answer adversarially, before it reaches the user.',\n 'Be skeptical: your job is to catch what the draft got wrong, not to praise it.',\n '',\n `USER TASK: ${task.slice(0, 800)}`,\n `TOOLS YOU ACTUALLY RAN THIS TURN: ${toolsUsed.length ? toolsUsed.join(', ') : '(none)'}`,\n '',\n 'YOUR DRAFT ANSWER:',\n '\"\"\"',\n draft.slice(0, DRAFT_CAP),\n '\"\"\"',\n '',\n 'Find ONLY concrete problems in these three classes:',\n ' 1. CLAIMED-BUT-UNVERIFIED: the draft states something happened or is true that the tools run above do NOT actually support.',\n ' 2. LIKELY-WRONG: a factual/logical error a careful reader would catch.',\n ' 3. UNSTATED-RISK: a caveat or failure case the user needs but the draft omits.',\n '',\n 'If the draft is solid, reply with exactly: SOLID',\n 'Otherwise reply with 1-3 short bullet lines, each starting with \"- \", naming a real issue. No preamble.',\n ].join('\\n');\n}\n\nexport interface CritiqueVerdict {\n solid: boolean;\n issues: string[];\n}\n\n/** Parse a critique response. Pure. */\nexport function parseCritique(response: string): CritiqueVerdict {\n const text = (response || '').trim();\n if (!text || /^solid\\b/i.test(text)) return { solid: true, issues: [] };\n const issues = text\n .split('\\n')\n .map(l => l.trim())\n .filter(l => l.startsWith('-'))\n .map(l => l.replace(/^-\\s*/, '').trim())\n .filter(Boolean)\n .slice(0, 3);\n // A response with no bullets and no \"SOLID\" is ambiguous → treat as solid\n // (don't append noise on a malformed critique).\n if (issues.length === 0) return { solid: true, issues: [] };\n return { solid: false, issues };\n}\n\n/** Format the appended caveat. Pure. */\nexport function formatReflection(issues: string[]): string {\n if (issues.length === 0) return '';\n if (issues.length === 1) return `\\n\\n_On reflection: ${issues[0]}_`;\n return `\\n\\n_On reflection, a couple of caveats:_\\n${issues.map(i => `- ${i}`).join('\\n')}`;\n}\n\n/**\n * Run the critique pass. Returns the (possibly caveated) content + whether it\n * fired. Never throws — a failed critique returns the draft unchanged.\n */\nexport async function runSelfCritique(\n cfg: SelfCritiqueConfig,\n task: string,\n draft: string,\n toolsUsed: string[],\n turnModel: string,\n): Promise<{ content: string; critiqued: boolean; issues: string[] }> {\n if (!shouldCritique(cfg, draft, toolsUsed)) return { content: draft, critiqued: false, issues: [] };\n try {\n const { chat } = await import('../providers/router.js');\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), CRITIQUE_TIMEOUT_MS);\n let response: string;\n try {\n const res = await chat({\n model: cfg.model || turnModel,\n messages: [{ role: 'user', content: buildCritiquePrompt(task, draft, toolsUsed) }],\n maxTokens: CRITIQUE_MAX_TOKENS,\n temperature: 0.2,\n signal: controller.signal,\n } as Parameters<typeof chat>[0]);\n response = res.content || '';\n } finally {\n clearTimeout(timer);\n }\n const verdict = parseCritique(response);\n if (verdict.solid) return { content: draft, critiqued: true, issues: [] };\n logger.info(COMPONENT, `Self-critique surfaced ${verdict.issues.length} issue(s); appended reflection`);\n return { content: draft + formatReflection(verdict.issues), critiqued: true, issues: verdict.issues };\n } catch (e) {\n logger.debug(COMPONENT, `Self-critique skipped (error): ${(e as Error).message}`);\n return { content: draft, critiqued: false, issues: [] };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from "fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import logger from "../utils/logger.js";
|
|
5
5
|
import { TITAN_HOME } from "../utils/constants.js";
|
|
@@ -39,7 +39,24 @@ function logTrajectory(trajectory) {
|
|
|
39
39
|
function getRecentTrajectories(limit = 50, filter) {
|
|
40
40
|
if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
42
|
+
const fd = openSync(TASK_TRAJECTORIES_FILE, "r");
|
|
43
|
+
let content = "";
|
|
44
|
+
try {
|
|
45
|
+
const size = fstatSync(fd).size;
|
|
46
|
+
let want = 512 * 1024;
|
|
47
|
+
for (; ; ) {
|
|
48
|
+
const start = Math.max(0, size - want);
|
|
49
|
+
const buf = Buffer.alloc(size - start);
|
|
50
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
51
|
+
content = buf.toString("utf-8");
|
|
52
|
+
if (start > 0) content = content.slice(content.indexOf("\n") + 1);
|
|
53
|
+
const enough = content.split("\n").filter((l) => l.trim()).length >= limit * 2;
|
|
54
|
+
if (enough || start === 0) break;
|
|
55
|
+
want *= 4;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
}
|
|
43
60
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
44
61
|
let trajectories = [];
|
|
45
62
|
for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync } from 'fs';\nimport { join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(TITAN_HOME, 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n const
|
|
1
|
+
{"version":3,"sources":["../../src/agent/trajectoryLogger.ts"],"sourcesContent":["/**\n * TITAN — Task-Level Trajectory Logger\n *\n * Records task-level trajectories (task, model, tool sequence, success, rounds, duration)\n * to ~/.titan/trajectories/task-trajectories.jsonl for auto-skill generation.\n *\n * Separate from trajectoryCapture.ts which produces ChatML format for LoRA fine-tuning.\n * Inspired by Hermes trajectory.py.\n */\nimport { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'fs';\nimport { join } from 'path';\nimport logger from '../utils/logger.js';\nimport { TITAN_HOME } from '../utils/constants.js';\n\nconst COMPONENT = 'TrajectoryLogger';\nconst TRAJECTORIES_DIR = join(TITAN_HOME, 'trajectories');\nconst TASK_TRAJECTORIES_FILE = join(TRAJECTORIES_DIR, 'task-trajectories.jsonl');\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface TaskTrajectory {\n id: string;\n timestamp: string;\n /** First 500 chars of the user message */\n task: string;\n taskType: string;\n model: string;\n /** Ordered tool names as executed */\n toolSequence: string[];\n /** Detailed tool call records */\n toolDetails: Array<{\n name: string;\n args: Record<string, unknown>;\n success: boolean;\n resultSnippet: string;\n }>;\n success: boolean;\n rounds: number;\n durationMs: number;\n sessionId: string;\n}\n\n// ── Ensure directory exists ───────────────────────────────────────\nfunction ensureDir(): void {\n if (!existsSync(TRAJECTORIES_DIR)) mkdirSync(TRAJECTORIES_DIR, { recursive: true });\n}\n\n// ── Auto-rotate ───────────────────────────────────────────────────\nfunction rotateIfNeeded(): void {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return;\n try {\n const stat = statSync(TASK_TRAJECTORIES_FILE);\n if (stat.size > MAX_FILE_SIZE) {\n logger.warn(COMPONENT, `Task trajectories file exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB — rotating`);\n const rotated = `${TASK_TRAJECTORIES_FILE}.${Date.now()}.bak`;\n try { writeFileSync(rotated, readFileSync(TASK_TRAJECTORIES_FILE)); } catch { /* ignore */ }\n writeFileSync(TASK_TRAJECTORIES_FILE, '');\n }\n } catch { /* ignore stat errors */ }\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\n/**\n * Log a task-level trajectory.\n * Fire-and-forget — errors are logged but never thrown.\n */\nexport function logTrajectory(trajectory: TaskTrajectory): void {\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(TASK_TRAJECTORIES_FILE, JSON.stringify(trajectory) + '\\n', 'utf-8');\n logger.debug(COMPONENT, `Logged trajectory: ${trajectory.taskType}, ${trajectory.toolSequence.length} tools, success=${trajectory.success}`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to log trajectory: ${(err as Error).message}`);\n }\n}\n\n/**\n * Read recent trajectories, optionally filtered.\n */\nexport function getRecentTrajectories(\n limit: number = 50,\n filter?: { taskType?: string; success?: boolean },\n): TaskTrajectory[] {\n if (!existsSync(TASK_TRAJECTORIES_FILE)) return [];\n\n try {\n // PERF (v7.1.x): this runs on EVERY message (auto-skill gate) and the\n // file grows to 50MB before rotation — reading it whole cost tens of\n // ms + a 50MB string per turn. Read only the TAIL: start with 512KB\n // and double until we have enough parseable lines or hit the start.\n const fd = openSync(TASK_TRAJECTORIES_FILE, 'r');\n let content = '';\n try {\n const size = fstatSync(fd).size;\n let want = 512 * 1024;\n for (;;) {\n const start = Math.max(0, size - want);\n const buf = Buffer.alloc(size - start);\n readSync(fd, buf, 0, buf.length, start);\n content = buf.toString('utf-8');\n // Drop the first (possibly truncated) line unless we read from 0\n if (start > 0) content = content.slice(content.indexOf('\\n') + 1);\n const enough = content.split('\\n').filter(l => l.trim()).length >= limit * 2;\n if (enough || start === 0) break;\n want *= 4;\n }\n } finally {\n closeSync(fd);\n }\n const lines = content.split('\\n').filter(l => l.trim());\n let trajectories: TaskTrajectory[] = [];\n\n // Read from the end for efficiency\n for (let i = lines.length - 1; i >= 0 && trajectories.length < limit * 2; i--) {\n try {\n trajectories.push(JSON.parse(lines[i]));\n } catch { /* skip malformed lines */ }\n }\n\n // Apply filters\n if (filter?.taskType) {\n trajectories = trajectories.filter(t => t.taskType === filter.taskType);\n }\n if (filter?.success !== undefined) {\n trajectories = trajectories.filter(t => t.success === filter.success);\n }\n\n return trajectories.slice(0, limit);\n } catch {\n return [];\n }\n}\n\n/**\n * Get the tool sequence signature for matching (joined with →).\n */\nexport function getSequenceSignature(toolSequence: string[]): string {\n return toolSequence.join(' → ');\n}\n\n/**\n * Count how many successful trajectories match a given task type and tool sequence.\n */\nexport function countMatchingTrajectories(taskType: string, toolSequence: string[]): number {\n const recent = getRecentTrajectories(200, { taskType, success: true });\n const targetSig = getSequenceSignature(toolSequence);\n return recent.filter(t => getSequenceSignature(t.toolSequence) === targetSig).length;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analytics/collector.ts"],"sourcesContent":["/**\n * TITAN — Analytics Collector\n * Collects anonymized system profile and usage heartbeat for product analytics.\n * All data is privacy-respecting: no IP, no personal data, no conversation content.\n */\nimport { platform, arch, release, cpus, totalmem } from 'os';\nimport { detectHardware } from '../hardware/autoConfig.js';\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { sendPostHogEvent, getPostHogStatus } from './posthog.js';\n\nconst COMPONENT = 'Analytics';\n\nexport interface SystemProfile {\n /** Anonymous install ID — stable per ~/.titan/ */\n installId: string;\n /** TITAN version */\n version: string;\n /** Node.js version */\n nodeVersion: string;\n /** OS platform */\n os: string;\n /** OS release */\n osRelease: string;\n /** CPU architecture */\n arch: string;\n /** CPU cores */\n cpuCores: number;\n /** Total RAM in GB (bucketed to nearest GB) */\n ramTotalGB: number;\n /** GPU vendor */\n gpuVendor: string;\n /** GPU VRAM in GB (bucketed to nearest GB) */\n gpuVramGB: number;\n /** Install method: 'git' | 'npm' | 'unknown' */\n installMethod: string;\n /** Total disk GB (bucketed to nearest 10GB) */\n diskTotalGB: number;\n /** Timestamp of collection */\n collectedAt: string;\n}\n\nexport interface HeartbeatEvent {\n installId: string;\n version: string;\n event: 'heartbeat' | 'startup' | 'shutdown';\n timestamp: string;\n uptimeSeconds: number;\n activeSessions: number;\n /** Feature adoption snapshot — which major subsystems are enabled */\n features?: {\n voice: boolean;\n mesh: boolean;\n brain: boolean;\n tunnel: boolean;\n autopilot: boolean;\n selfImprove: boolean;\n training: boolean;\n daemon: boolean;\n nvidia: boolean;\n organism: boolean;\n selfMod: boolean;\n commandPost: boolean;\n mcpServer: boolean;\n /** Count of enabled channel adapters */\n channelsEnabled: number;\n /** Count of providers with at least one auth profile or apiKey */\n providersConfigured: number;\n };\n}\n\n/** Detect how TITAN was installed */\nasync function detectInstallMethod(): Promise<'git' | 'npm' | 'unknown'> {\n try {\n const { existsSync } = await import('fs');\n const { join } = await import('path');\n if (existsSync(join(process.cwd(), '.git'))) return 'git';\n if (process.cwd().includes('node_modules') || process.cwd().includes('/lib/node')) return 'npm';\n } catch { /* ignore */ }\n return 'unknown';\n}\n\n/** Bucket a value to the nearest step */\nfunction bucketToNearest(value: number, step: number): number {\n return Math.round(value / step) * step;\n}\n\n/** Collect full system profile (privacy-respecting: no cpuModel/gpuName, bucketed sizes) */\nexport async function collectSystemProfile(): Promise<SystemProfile> {\n const hw = await detectHardware();\n const installMethod = await detectInstallMethod();\n const cpuList = cpus();\n\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n nodeVersion: process.version,\n os: platform(),\n osRelease: release(),\n arch: arch(),\n cpuCores: cpuList.length || hw.cpuCores,\n ramTotalGB: bucketToNearest(hw.ramTotalMB / 1024, 4),\n gpuVendor: hw.gpuVendor,\n gpuVramGB: bucketToNearest(hw.gpuVramMB / 1024, 4),\n installMethod,\n diskTotalGB: bucketToNearest(hw.diskTotalGB, 50),\n collectedAt: new Date().toISOString(),\n };\n}\n\n/** Snapshot of which major features are enabled in the current config */\nfunction getFeatureSnapshot(cfg: ReturnType<typeof loadConfig>): HeartbeatEvent['features'] {\n const channels = cfg.channels || {};\n const providers = cfg.providers || {};\n const chEnabled = Object.values(channels).filter((c: unknown) => (c as Record<string, unknown>)?.enabled === true).length;\n const provConfigured = Object.entries(providers).filter(([, p]) => {\n const pr = p as Record<string, unknown>;\n return Boolean(pr?.apiKey) || (Array.isArray(pr?.authProfiles) && (pr.authProfiles as unknown[]).length > 0);\n }).length;\n return {\n voice: cfg.voice?.enabled === true,\n mesh: cfg.mesh?.enabled === true,\n brain: cfg.brain?.enabled === true,\n tunnel: cfg.tunnel?.enabled === true,\n autopilot: cfg.autopilot?.enabled === true,\n selfImprove: cfg.selfImprove?.enabled === true,\n training: cfg.training?.enabled === true,\n daemon: cfg.daemon?.enabled === true,\n nvidia: cfg.nvidia?.enabled === true,\n organism: cfg.organism?.enabled === true,\n selfMod: cfg.selfMod?.enabled === true,\n commandPost: cfg.commandPost?.enabled === true,\n mcpServer: cfg.mcp?.server?.enabled === true,\n channelsEnabled: chEnabled,\n providersConfigured: provConfigured,\n };\n}\n\n/** Build a heartbeat event */\nexport function buildHeartbeat(activeSessions = 0): HeartbeatEvent {\n const cfg = loadConfig();\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n event: 'heartbeat',\n timestamp: new Date().toISOString(),\n uptimeSeconds: Math.round(process.uptime()),\n activeSessions,\n features: getFeatureSnapshot(cfg),\n };\n}\n\n/**\n * Remote-send status. Exposed so the gateway and Mission Control can\n * show Tony whether telemetry is actually landing anywhere. Prior to\n * v5.0 the send function swallowed errors silently — the new state\n * struct records the last attempt so the Privacy widget can surface\n * \"last sent 14s ago ✓\" vs \"last send failed: connection refused\".\n */\nexport interface RemoteAnalyticsStatus {\n configured: boolean;\n remoteUrl?: string;\n sentCount: number;\n failedCount: number;\n lastAttemptAt?: string;\n lastSuccessAt?: string;\n lastError?: string;\n lastEventType?: string;\n /** PostHog bridge status (parallel send) */\n posthog?: { enabled: boolean; sentCount: number; failedCount: number; lastError?: string; lastSuccessAt?: string };\n}\nconst remoteStatus: RemoteAnalyticsStatus = {\n configured: false,\n sentCount: 0,\n failedCount: 0,\n};\nexport function getRemoteAnalyticsStatus(): RemoteAnalyticsStatus {\n return { ...remoteStatus, posthog: getPostHogStatus() };\n}\n\n/** Send analytics event to remote endpoint if configured */\nexport async function sendRemoteAnalytics(payload: Record<string, unknown>): Promise<void> {\n const cfg = loadConfig();\n const remoteUrl = (cfg.telemetry as Record<string, unknown> | undefined)?.remoteUrl as string | undefined;\n remoteStatus.configured = Boolean(remoteUrl);\n remoteStatus.remoteUrl = remoteUrl;\n if (!remoteUrl) return;\n\n const now = new Date().toISOString();\n remoteStatus.lastAttemptAt = now;\n remoteStatus.lastEventType = typeof payload.type === 'string' ? payload.type : 'unknown';\n try {\n const res = await fetch(remoteUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n remoteStatus.sentCount += 1;\n remoteStatus.lastSuccessAt = now;\n remoteStatus.lastError = undefined;\n } catch (err) {\n remoteStatus.failedCount += 1;\n remoteStatus.lastError = (err as Error).message || String(err);\n // Remote analytics is best-effort — we don't throw, but the\n // status struct lets callers see the failure.\n }\n\n // Parallel send to PostHog (if configured) — never blocks or throws.\n sendPostHogEvent(payload).catch(() => {});\n}\n\n/**\n * Read the postinstall marker (written by `scripts/postinstall.cjs`) and,\n * if the user has consented to telemetry, fire a one-shot `install` or\n * `update` event the first time the gateway boots after an install/upgrade.\n *\n * The marker stays on disk regardless of consent; we just won't SEND anything\n * until the user opts in. The `reported: true` flag prevents re-sending after\n * a gateway restart.\n */\nasync function reportInstallMarkerOnce(): Promise<void> {\n try {\n const { readFileSync, writeFileSync, existsSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const markerPath = join(homedir(), '.titan', 'install-marker.json');\n if (!existsSync(markerPath)) return;\n\n type Marker = {\n installedVersion?: string;\n previousVersion?: string | null;\n installedAt?: string;\n installMethod?: string;\n reported?: boolean;\n };\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as Marker;\n if (marker.reported) return;\n\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return; // Stays unreported until consent\n\n const isUpdate = Boolean(marker.previousVersion && marker.previousVersion !== marker.installedVersion);\n await sendRemoteAnalytics({\n type: isUpdate ? 'update' : 'install',\n installId: getOrCreateNodeId(),\n version: marker.installedVersion || TITAN_VERSION,\n fromVersion: isUpdate ? marker.previousVersion : null,\n installMethod: marker.installMethod || 'unknown',\n installedAt: marker.installedAt || new Date().toISOString(),\n });\n\n writeFileSync(markerPath, JSON.stringify({ ...marker, reported: true }, null, 2), 'utf-8');\n logger.info(COMPONENT, `Reported ${isUpdate ? 'update' : 'install'} event for ${marker.installedVersion}`);\n } catch (e) {\n logger.debug(COMPONENT, `Install-marker reporter skipped: ${(e as Error).message}`);\n }\n}\n\n/** Record startup profile + heartbeat via local telemetry */\nexport async function recordStartupAnalytics(): Promise<void> {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n try {\n const profile = await collectSystemProfile();\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'system_profile',\n properties: profile as unknown as Record<string, unknown>,\n timestamp: new Date().toISOString(),\n sessionId: profile.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'system_profile',\n ...profile,\n });\n\n logger.info(COMPONENT, `System profile recorded: ${profile.os} ${profile.arch}, ${profile.cpuCores}c/${profile.ramTotalGB}GB, GPU: ${profile.gpuVendor || 'none'}`);\n\n // Fire install/update event once per new install — only when consented.\n await reportInstallMarkerOnce();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to record startup analytics: ${(e as Error).message}`);\n }\n}\n\n/** Schedule periodic heartbeat analytics */\nexport function startHeartbeatAnalytics(activeSessionsProvider: () => number): void {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n const HEARTBEAT_INTERVAL_MS = 300_000; // 5 minutes\n\n const tick = async () => {\n try {\n const heartbeat = buildHeartbeat(activeSessionsProvider());\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'heartbeat',\n properties: heartbeat as unknown as Record<string, unknown>,\n timestamp: heartbeat.timestamp,\n sessionId: heartbeat.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'heartbeat',\n ...heartbeat,\n });\n } catch {\n // Best-effort\n }\n };\n\n tick();\n setInterval(tick, HEARTBEAT_INTERVAL_MS);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/collector.ts"],"sourcesContent":["/**\n * TITAN — Analytics Collector\n * Collects anonymized system profile and usage heartbeat for product analytics.\n * All data is privacy-respecting: no IP, no personal data, no conversation content.\n */\nimport { platform, arch, release, cpus, totalmem } from 'os';\nimport { detectHardware } from '../hardware/autoConfig.js';\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\nimport { sendPostHogEvent, getPostHogStatus } from './posthog.js';\n\nconst COMPONENT = 'Analytics';\n\nexport interface SystemProfile {\n /** Anonymous install ID — stable per ~/.titan/ */\n installId: string;\n /** TITAN version */\n version: string;\n /** Node.js version */\n nodeVersion: string;\n /** OS platform */\n os: string;\n /** OS release */\n osRelease: string;\n /** CPU architecture */\n arch: string;\n /** CPU cores */\n cpuCores: number;\n /** Total RAM in GB (bucketed to nearest GB) */\n ramTotalGB: number;\n /** GPU vendor */\n gpuVendor: string;\n /** GPU VRAM in GB (bucketed to nearest GB) */\n gpuVramGB: number;\n /** Install method: 'git' | 'npm' | 'unknown' */\n installMethod: string;\n /** Total disk GB (bucketed to nearest 10GB) */\n diskTotalGB: number;\n /** Timestamp of collection */\n collectedAt: string;\n}\n\nexport interface HeartbeatEvent {\n installId: string;\n version: string;\n event: 'heartbeat' | 'startup' | 'shutdown';\n timestamp: string;\n uptimeSeconds: number;\n activeSessions: number;\n /** Feature adoption snapshot — which major subsystems are enabled */\n features?: {\n voice: boolean;\n mesh: boolean;\n brain: boolean;\n tunnel: boolean;\n autopilot: boolean;\n selfImprove: boolean;\n training: boolean;\n daemon: boolean;\n nvidia: boolean;\n organism: boolean;\n selfMod: boolean;\n commandPost: boolean;\n mcpServer: boolean;\n /** Count of enabled channel adapters */\n channelsEnabled: number;\n /** Count of providers with at least one auth profile or apiKey */\n providersConfigured: number;\n };\n}\n\n/** Detect how TITAN was installed */\nasync function detectInstallMethod(): Promise<'git' | 'npm' | 'unknown'> {\n try {\n const { existsSync } = await import('fs');\n const { join } = await import('path');\n if (existsSync(join(process.cwd(), '.git'))) return 'git';\n if (process.cwd().includes('node_modules') || process.cwd().includes('/lib/node')) return 'npm';\n } catch { /* ignore */ }\n return 'unknown';\n}\n\n/** Bucket a value to the nearest step */\nfunction bucketToNearest(value: number, step: number): number {\n return Math.round(value / step) * step;\n}\n\n/** Collect full system profile (privacy-respecting: no cpuModel/gpuName, bucketed sizes) */\nexport async function collectSystemProfile(): Promise<SystemProfile> {\n const hw = await detectHardware();\n const installMethod = await detectInstallMethod();\n const cpuList = cpus();\n\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n nodeVersion: process.version,\n os: platform(),\n osRelease: release(),\n arch: arch(),\n cpuCores: cpuList.length || hw.cpuCores,\n ramTotalGB: bucketToNearest(hw.ramTotalMB / 1024, 4),\n gpuVendor: hw.gpuVendor,\n gpuVramGB: bucketToNearest(hw.gpuVramMB / 1024, 4),\n installMethod,\n diskTotalGB: bucketToNearest(hw.diskTotalGB, 50),\n collectedAt: new Date().toISOString(),\n };\n}\n\n/** Snapshot of which major features are enabled in the current config */\nfunction getFeatureSnapshot(cfg: ReturnType<typeof loadConfig>): HeartbeatEvent['features'] {\n const channels = cfg.channels || {};\n const providers = cfg.providers || {};\n const chEnabled = Object.values(channels).filter((c: unknown) => (c as Record<string, unknown>)?.enabled === true).length;\n const provConfigured = Object.entries(providers).filter(([, p]) => {\n const pr = p as Record<string, unknown>;\n return Boolean(pr?.apiKey) || (Array.isArray(pr?.authProfiles) && (pr.authProfiles as unknown[]).length > 0);\n }).length;\n return {\n voice: cfg.voice?.enabled === true,\n mesh: cfg.mesh?.enabled === true,\n brain: cfg.brain?.enabled === true,\n tunnel: cfg.tunnel?.enabled === true,\n autopilot: cfg.autopilot?.enabled === true,\n selfImprove: cfg.selfImprove?.enabled === true,\n training: cfg.training?.enabled === true,\n daemon: cfg.daemon?.enabled === true,\n nvidia: cfg.nvidia?.enabled === true,\n organism: cfg.organism?.enabled === true,\n selfMod: cfg.selfMod?.enabled === true,\n commandPost: cfg.commandPost?.enabled === true,\n mcpServer: cfg.mcp?.server?.enabled === true,\n channelsEnabled: chEnabled,\n providersConfigured: provConfigured,\n };\n}\n\n/** Build a heartbeat event */\nexport function buildHeartbeat(activeSessions = 0): HeartbeatEvent {\n const cfg = loadConfig();\n return {\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n event: 'heartbeat',\n timestamp: new Date().toISOString(),\n uptimeSeconds: Math.round(process.uptime()),\n activeSessions,\n features: getFeatureSnapshot(cfg),\n };\n}\n\n/**\n * Remote-send status. Exposed so the gateway and Mission Control can\n * show Tony whether telemetry is actually landing anywhere. Prior to\n * v5.0 the send function swallowed errors silently — the new state\n * struct records the last attempt so the Privacy widget can surface\n * \"last sent 14s ago ✓\" vs \"last send failed: connection refused\".\n */\nexport interface RemoteAnalyticsStatus {\n configured: boolean;\n remoteUrl?: string;\n sentCount: number;\n failedCount: number;\n lastAttemptAt?: string;\n lastSuccessAt?: string;\n lastError?: string;\n lastEventType?: string;\n /** PostHog bridge status (parallel send) */\n posthog?: { enabled: boolean; sentCount: number; failedCount: number; lastError?: string; lastSuccessAt?: string };\n}\nconst remoteStatus: RemoteAnalyticsStatus = {\n configured: false,\n sentCount: 0,\n failedCount: 0,\n};\nexport function getRemoteAnalyticsStatus(): RemoteAnalyticsStatus {\n return { ...remoteStatus, posthog: getPostHogStatus() };\n}\n\n/** Send analytics event to remote endpoint if configured */\nexport async function sendRemoteAnalytics(payload: Record<string, unknown>): Promise<void> {\n const cfg = loadConfig();\n const remoteUrl = (cfg.telemetry as Record<string, unknown> | undefined)?.remoteUrl as string | undefined;\n remoteStatus.configured = Boolean(remoteUrl);\n remoteStatus.remoteUrl = remoteUrl;\n if (!remoteUrl) return;\n\n const now = new Date().toISOString();\n remoteStatus.lastAttemptAt = now;\n remoteStatus.lastEventType = typeof payload.type === 'string' ? payload.type : 'unknown';\n try {\n const res = await fetch(remoteUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n remoteStatus.sentCount += 1;\n remoteStatus.lastSuccessAt = now;\n remoteStatus.lastError = undefined;\n } catch (err) {\n remoteStatus.failedCount += 1;\n remoteStatus.lastError = (err as Error).message || String(err);\n // Remote analytics is best-effort — we don't throw, but the\n // status struct lets callers see the failure.\n }\n\n // Parallel send to PostHog (if configured) — never blocks or throws.\n sendPostHogEvent(payload).catch(() => {});\n}\n\n/**\n * Read the postinstall marker (written by `scripts/postinstall.cjs`) and,\n * if the user has consented to telemetry, fire a one-shot `install` or\n * `update` event the first time the gateway boots after an install/upgrade.\n *\n * The marker stays on disk regardless of consent; we just won't SEND anything\n * until the user opts in. The `reported: true` flag prevents re-sending after\n * a gateway restart.\n */\nasync function reportInstallMarkerOnce(): Promise<void> {\n try {\n const { readFileSync, writeFileSync, existsSync } = await import('fs');\n const { join } = await import('path');\n const { homedir } = await import('os');\n const markerPath = join(homedir(), '.titan', 'install-marker.json');\n if (!existsSync(markerPath)) return;\n\n type Marker = {\n installedVersion?: string;\n previousVersion?: string | null;\n installedAt?: string;\n installMethod?: string;\n reported?: boolean;\n };\n const marker = JSON.parse(readFileSync(markerPath, 'utf-8')) as Marker;\n if (marker.reported) return;\n\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return; // Stays unreported until consent\n\n const isUpdate = Boolean(marker.previousVersion && marker.previousVersion !== marker.installedVersion);\n await sendRemoteAnalytics({\n type: isUpdate ? 'update' : 'install',\n installId: getOrCreateNodeId(),\n version: marker.installedVersion || TITAN_VERSION,\n fromVersion: isUpdate ? marker.previousVersion : null,\n installMethod: marker.installMethod || 'unknown',\n installedAt: marker.installedAt || new Date().toISOString(),\n });\n\n writeFileSync(markerPath, JSON.stringify({ ...marker, reported: true }, null, 2), 'utf-8');\n logger.info(COMPONENT, `Reported ${isUpdate ? 'update' : 'install'} event for ${marker.installedVersion}`);\n } catch (e) {\n logger.debug(COMPONENT, `Install-marker reporter skipped: ${(e as Error).message}`);\n }\n}\n\n/** Record startup profile + heartbeat via local telemetry */\nexport async function recordStartupAnalytics(): Promise<void> {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n try {\n const profile = await collectSystemProfile();\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'system_profile',\n properties: profile as unknown as Record<string, unknown>,\n timestamp: new Date().toISOString(),\n sessionId: profile.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'system_profile',\n ...profile,\n });\n\n logger.info(COMPONENT, `System profile recorded: ${profile.os} ${profile.arch}, ${profile.cpuCores}c/${profile.ramTotalGB}GB, GPU: ${profile.gpuVendor || 'none'}`);\n\n // Fire install/update event once per new install — only when consented.\n await reportInstallMarkerOnce();\n } catch (e) {\n logger.warn(COMPONENT, `Failed to record startup analytics: ${(e as Error).message}`);\n }\n}\n\n/** Schedule periodic heartbeat analytics */\nexport function startHeartbeatAnalytics(activeSessionsProvider: () => number): void {\n const cfg = loadConfig();\n if (!cfg.telemetry?.enabled) return;\n\n const HEARTBEAT_INTERVAL_MS = 300_000; // 5 minutes\n\n const tick = async () => {\n try {\n const heartbeat = buildHeartbeat(activeSessionsProvider());\n const { getStorage } = await import('../storage/index.js');\n const storage = await getStorage();\n\n await storage.appendTelemetryEvent?.({\n event: 'heartbeat',\n properties: heartbeat as unknown as Record<string, unknown>,\n timestamp: heartbeat.timestamp,\n sessionId: heartbeat.installId,\n });\n\n await sendRemoteAnalytics({\n type: 'heartbeat',\n ...heartbeat,\n });\n } catch {\n // Best-effort\n }\n };\n\n tick();\n setInterval(tick, HEARTBEAT_INTERVAL_MS).unref();\n}\n"],"mappings":"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","names":[]}
|
package/dist/config/schema.js
CHANGED
|
@@ -207,6 +207,20 @@ const AgentConfigSchema = z.object({
|
|
|
207
207
|
/** Active persona ID (filename stem from assets/personas/). Default 'default' = no persona override. */
|
|
208
208
|
persona: z.string().default("default"),
|
|
209
209
|
workspace: z.string().optional(),
|
|
210
|
+
/** v7.1.x Organ 5 — adversarial self-critique (Reflexion). OFF by default.
|
|
211
|
+
* When on, a substantive turn gets one bounded critique pass; real issues
|
|
212
|
+
* become an appended "On reflection" caveat. Trades latency for reliability
|
|
213
|
+
* — worth it for building/ops work on a local model. */
|
|
214
|
+
/** v7.2 one-switch "best harness" preset: turns on the honesty organs
|
|
215
|
+
* (self-critique; the verification wall is always on) without editing
|
|
216
|
+
* nested config. Trades a little latency for Fable-5-class reliability. */
|
|
217
|
+
reliabilityMode: z.boolean().default(false),
|
|
218
|
+
selfCritique: z.object({
|
|
219
|
+
enabled: z.boolean().optional(),
|
|
220
|
+
minToolCalls: z.number().int().min(0).default(1),
|
|
221
|
+
minDraftChars: z.number().int().min(0).default(200),
|
|
222
|
+
model: z.string().default("")
|
|
223
|
+
}).default({}),
|
|
210
224
|
sessionCompaction: z.object({
|
|
211
225
|
enabled: z.boolean().default(true).describe("Auto-rotate sessions when limits exceeded"),
|
|
212
226
|
maxSessionRuns: z.number().default(200).describe("Max interactions per session before rotation"),
|