instar 1.3.753 → 1.3.755
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/commands/server.d.ts +30 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +238 -19
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +40 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/ProfileIntentClassifier.d.ts +167 -0
- package/dist/core/ProfileIntentClassifier.d.ts.map +1 -0
- package/dist/core/ProfileIntentClassifier.js +421 -0
- package/dist/core/ProfileIntentClassifier.js.map +1 -0
- package/dist/core/componentCategories.d.ts.map +1 -1
- package/dist/core/componentCategories.js +11 -0
- package/dist/core/componentCategories.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +12 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/topicProfileIngress.d.ts +14 -2
- package/dist/core/topicProfileIngress.d.ts.map +1 -1
- package/dist/core/topicProfileIngress.js +15 -44
- package/dist/core/topicProfileIngress.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +15 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/dist/threadline/HubIntentClassifier.d.ts +150 -0
- package/dist/threadline/HubIntentClassifier.d.ts.map +1 -0
- package/dist/threadline/HubIntentClassifier.js +322 -0
- package/dist/threadline/HubIntentClassifier.js.map +1 -0
- package/dist/threadline/hubCommands.d.ts +16 -12
- package/dist/threadline/hubCommands.d.ts.map +1 -1
- package/dist/threadline/hubCommands.js +16 -30
- package/dist/threadline/hubCommands.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/llmBenchCoverage.ts +16 -0
- package/upgrades/1.3.754.md +46 -0
- package/upgrades/1.3.755.md +75 -0
- package/upgrades/side-effects/keyword-intent-conversion-3-hubcommands.md +170 -0
- package/upgrades/side-effects/topicprofile-intent-llm-rebuild.md +87 -0
|
@@ -11,6 +11,7 @@ import { SessionManager } from '../core/SessionManager.js';
|
|
|
11
11
|
import { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
12
12
|
import { TopicMemory } from '../memory/TopicMemory.js';
|
|
13
13
|
import { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
14
|
+
import type { IntelligenceProvider, InstarConfig } from '../core/types.js';
|
|
14
15
|
import { UserManager } from '../users/UserManager.js';
|
|
15
16
|
/**
|
|
16
17
|
* Pure: should the emergency "fix command" gate intercept this message?
|
|
@@ -38,7 +39,35 @@ interface StartOptions {
|
|
|
38
39
|
* Commander maps --no-telegram to telegram: false. */
|
|
39
40
|
telegram?: boolean;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the hub-intent classifier deps (Conversion #3,
|
|
44
|
+
* docs/specs/keyword-intent-conversions-1-and-3.md) from live config + the shared
|
|
45
|
+
* IntelligenceProvider. `enabled` is DEV-GATED (resolveDevAgentGate): DARK on the
|
|
46
|
+
* fleet, LIVE on a development agent. `dryRun` defaults true (the soak) so a
|
|
47
|
+
* dev-agent classifier RUNS but the message ALWAYS passes through until a
|
|
48
|
+
* deliberate `dryRun:false`. Passed as a late-bound getter so it re-reads config
|
|
49
|
+
* at message time.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveHubClassifierDeps(config: InstarConfig, intelligence: IntelligenceProvider | null): {
|
|
52
|
+
intelligence: IntelligenceProvider | null;
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
dryRun: boolean;
|
|
55
|
+
minConfidence: number;
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
contextTurns: number;
|
|
58
|
+
modelTier: 'fast' | 'balanced' | 'capable';
|
|
59
|
+
stateDir: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function wireTelegramRouting(telegram: TelegramAdapter, sessionManager: SessionManager, quotaTracker?: QuotaTracker, topicMemory?: TopicMemory, userManager?: UserManager, fixCommandHandler?: (topicId: number, text: string) => Promise<boolean>, getHubDeps?: () => import('../threadline/hubCommands.js').HubBindDeps | null, getTopicOperatorStore?: () => import('../users/TopicOperatorStore.js').TopicOperatorStore | null, getAttentionTopicId?: () => number | null | undefined, getHubClassifierDeps?: () => {
|
|
62
|
+
intelligence: import('../core/types.js').IntelligenceProvider | null;
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
dryRun: boolean;
|
|
65
|
+
minConfidence: number;
|
|
66
|
+
timeoutMs: number;
|
|
67
|
+
contextTurns: number;
|
|
68
|
+
modelTier: 'fast' | 'balanced' | 'capable';
|
|
69
|
+
stateDir: string;
|
|
70
|
+
} | null): void;
|
|
42
71
|
export declare function startServer(options: StartOptions): Promise<void>;
|
|
43
72
|
export declare function stopServer(options: {
|
|
44
73
|
dir?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAuDH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAU3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAyBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA6H7D,OAAO,KAAK,EAAW,oBAAoB,EAAe,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAgFtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAgmDD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,oBAAoB,GAAG,IAAI,GACxC;IACD,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,CAqBA;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,EAQrD,oBAAoB,CAAC,EAAE,MAAM;IAC3B,YAAY,EAAE,OAAO,kBAAkB,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,GACP,IAAI,CAonBN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAs0lBtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -32,6 +32,7 @@ import { PrHandLease } from '../core/PrHandLease.js';
|
|
|
32
32
|
import { ConversationRegistry } from '../core/ConversationRegistry.js';
|
|
33
33
|
import { claimSuspensionExcludesPin } from '../core/TopicClaimAnnotationStore.js';
|
|
34
34
|
import { parseProfileTrigger, platformMessageIdFrom } from '../core/topicProfileIngress.js';
|
|
35
|
+
import { classifyProfileIntent, toProfilePatch, } from '../core/ProfileIntentClassifier.js';
|
|
35
36
|
import { slugifyChannelName } from '../messaging/slack/sanitize.js';
|
|
36
37
|
import { TopicProfileOrchestrator, resolvedToApplied, } from '../core/TopicProfileOrchestrator.js';
|
|
37
38
|
import { CodexResumeMap } from '../core/CodexResumeMap.js';
|
|
@@ -704,6 +705,20 @@ function appendTopicProfileAudit(stateDir, event) {
|
|
|
704
705
|
}
|
|
705
706
|
let _projectDir = process.cwd();
|
|
706
707
|
let _sharedIntelligence = null;
|
|
708
|
+
// Offender #1 conversion (docs/specs/keyword-intent-conversions-1-and-3.md): the
|
|
709
|
+
// framework/model/thinking WRITE decision is inferred by the ProfileIntentClassifier
|
|
710
|
+
// (LLM + recent conversation), replacing the removed keyword regexes in
|
|
711
|
+
// parseProfileTrigger. Dev-gated dark (resolveDevAgentGate) + dry-run FIRST: while
|
|
712
|
+
// dark or dry-run the message ALWAYS passes through (never actuated); the classifier
|
|
713
|
+
// still runs on a dev agent and LOGS would-actuate vs would-pass to
|
|
714
|
+
// logs/profile-intent.jsonl for the soak. Resolved once in startServer.
|
|
715
|
+
let _profileIntentEnabled = false;
|
|
716
|
+
let _profileIntentDryRun = true;
|
|
717
|
+
let _profileIntentMinConfidence = 0.85;
|
|
718
|
+
let _profileIntentTimeoutMs = 4000;
|
|
719
|
+
let _profileIntentContextTurns = 6;
|
|
720
|
+
let _profileIntentModelTier = 'fast';
|
|
721
|
+
let _profileIntentStateDir = '';
|
|
707
722
|
let _selfKnowledgeTree = null;
|
|
708
723
|
let _slackAdapter = null;
|
|
709
724
|
// WS1.1 dispatch-to-owner (Slack arm): the owner-side bridge reconstructs a Slack
|
|
@@ -1669,6 +1684,42 @@ function messageToPipeline(msg, topicName) {
|
|
|
1669
1684
|
* a message carrying platform forward metadata falls through as normal
|
|
1670
1685
|
* conversation regardless of sender.
|
|
1671
1686
|
*/
|
|
1687
|
+
/**
|
|
1688
|
+
* Offender #1 conversion soak log: one JSONL line per LLM-engaged
|
|
1689
|
+
* ProfileIntentClassifier decision (docs/specs/keyword-intent-conversions-1-and-3.md).
|
|
1690
|
+
* A prefilter-skip is always a would-pass and the bulk of traffic — skipping it
|
|
1691
|
+
* cuts volume AND the privacy surface to messages that named a profile signal and
|
|
1692
|
+
* actually reached the classifier. Observability only — never gates the message.
|
|
1693
|
+
*/
|
|
1694
|
+
function logProfileIntent(topicId, text, result, acted) {
|
|
1695
|
+
try {
|
|
1696
|
+
if (result.source === 'prefilter-skip')
|
|
1697
|
+
return;
|
|
1698
|
+
if (!_profileIntentStateDir)
|
|
1699
|
+
return;
|
|
1700
|
+
const logDir = path.join(_profileIntentStateDir, 'logs');
|
|
1701
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
1702
|
+
const decision = result.isChange ? (acted ? 'actuate' : 'would-actuate') : 'pass';
|
|
1703
|
+
const line = JSON.stringify({
|
|
1704
|
+
ts: new Date().toISOString(),
|
|
1705
|
+
topicId,
|
|
1706
|
+
decision,
|
|
1707
|
+
dryRun: _profileIntentDryRun,
|
|
1708
|
+
source: result.source,
|
|
1709
|
+
intent: result.intent,
|
|
1710
|
+
value: result.value,
|
|
1711
|
+
confidence: result.confidence,
|
|
1712
|
+
reason: result.reason,
|
|
1713
|
+
// NO raw message content: the enum-bounded fields above are what the FP-rate
|
|
1714
|
+
// soak needs, and the resolved `value` is already a closed-enum member. We
|
|
1715
|
+
// record only the message LENGTH (non-content) — mirroring the codebase's
|
|
1716
|
+
// dev-soak convention of never persisting a raw operator quote.
|
|
1717
|
+
textLen: typeof text === 'string' ? text.length : 0,
|
|
1718
|
+
});
|
|
1719
|
+
fs.appendFileSync(path.join(logDir, 'profile-intent.jsonl'), line + '\n');
|
|
1720
|
+
}
|
|
1721
|
+
catch { /* @silent-fallback-ok: the soak audit is observability-only — a log write fault must never gate or throw out of the message path (the classifier decision already stands) */ }
|
|
1722
|
+
}
|
|
1672
1723
|
async function handleTopicProfileIngress(telegram, topicId, text, telegramUserId, msg) {
|
|
1673
1724
|
const surface = _topicProfileWriteSurface;
|
|
1674
1725
|
const slots = _topicProfileConfirmSlots;
|
|
@@ -1683,10 +1734,65 @@ async function handleTopicProfileIngress(telegram, topicId, text, telegramUserId
|
|
|
1683
1734
|
catch { /* @silent-fallback-ok: a trust-floor read fault fails toward NOT-a-trigger (deny-by-default) — the conversational profile parse never runs for an unauthorized turn (TOPIC-PROFILE-SPEC §10.1) */ }
|
|
1684
1735
|
if (!authorized)
|
|
1685
1736
|
return false;
|
|
1686
|
-
const trigger = parseProfileTrigger(text);
|
|
1687
|
-
if (!trigger)
|
|
1688
|
-
return false;
|
|
1689
1737
|
const forwarded = msg.metadata?.forwarded === true;
|
|
1738
|
+
const trigger = parseProfileTrigger(text);
|
|
1739
|
+
if (!trigger) {
|
|
1740
|
+
// Offender #1 conversion (docs/specs/keyword-intent-conversions-1-and-3.md):
|
|
1741
|
+
// the framework/model/thinking WRITE decision moved OUT of parseProfileTrigger
|
|
1742
|
+
// (keyword regexes) into the ProfileIntentClassifier — an LLM reasoning over
|
|
1743
|
+
// the message + recent conversation with structured-enum output, per the
|
|
1744
|
+
// constitutional standard "Intelligence Infers, Keywords Only Guard".
|
|
1745
|
+
// Dev-gated dark + dry-run FIRST; fail-OPEN on every uncertainty.
|
|
1746
|
+
if (forwarded)
|
|
1747
|
+
return false; // forwarded content is never a command
|
|
1748
|
+
if (!_profileIntentEnabled)
|
|
1749
|
+
return false; // fleet-dark → pass to the agent
|
|
1750
|
+
let conversationContext = [];
|
|
1751
|
+
try {
|
|
1752
|
+
// Fetch one extra turn, then drop a trailing turn equal to THIS message —
|
|
1753
|
+
// getTopicHistory (oldest→newest) usually already contains the just-logged
|
|
1754
|
+
// inbound row, so without this the model would see the message twice (once
|
|
1755
|
+
// as MESSAGE, once as the newest CONTEXT turn).
|
|
1756
|
+
const hist = telegram.getTopicHistory?.(topicId, _profileIntentContextTurns + 1) ?? [];
|
|
1757
|
+
conversationContext = hist.map((h) => ({ fromUser: !!h.fromUser, text: String(h.text ?? '') }));
|
|
1758
|
+
const trimmed = text.trim();
|
|
1759
|
+
while (conversationContext.length > 0
|
|
1760
|
+
&& conversationContext[conversationContext.length - 1].fromUser
|
|
1761
|
+
&& conversationContext[conversationContext.length - 1].text.trim() === trimmed) {
|
|
1762
|
+
conversationContext.pop();
|
|
1763
|
+
}
|
|
1764
|
+
conversationContext = conversationContext.slice(-_profileIntentContextTurns);
|
|
1765
|
+
}
|
|
1766
|
+
catch { /* @silent-fallback-ok — context is best-effort; classify on the message alone */ }
|
|
1767
|
+
const result = await classifyProfileIntent({
|
|
1768
|
+
text,
|
|
1769
|
+
conversationContext,
|
|
1770
|
+
intelligence: _sharedIntelligence,
|
|
1771
|
+
minConfidence: _profileIntentMinConfidence,
|
|
1772
|
+
timeoutMs: _profileIntentTimeoutMs,
|
|
1773
|
+
maxContextTurns: _profileIntentContextTurns,
|
|
1774
|
+
modelTier: _profileIntentModelTier,
|
|
1775
|
+
});
|
|
1776
|
+
const willAct = result.isChange && !_profileIntentDryRun;
|
|
1777
|
+
logProfileIntent(topicId, text, result, willAct);
|
|
1778
|
+
// Dry-run (the soak) / fail-open / pass-through: log would-actuate vs
|
|
1779
|
+
// would-pass, but pass the message through untouched. Real actuation needs a
|
|
1780
|
+
// deliberate dryRun:false, AND fail-open means uncertainty never actuates.
|
|
1781
|
+
if (!willAct)
|
|
1782
|
+
return false;
|
|
1783
|
+
const patch = toProfilePatch(result);
|
|
1784
|
+
if (!patch)
|
|
1785
|
+
return false;
|
|
1786
|
+
const applyPrincipal = { kind: 'operator', platform: 'telegram', uid: String(telegramUserId) };
|
|
1787
|
+
const applyResult = await surface.applyWrite({
|
|
1788
|
+
topicKey: String(topicId), patch, principal: applyPrincipal, origin: 'conversational', discloseInReply: true,
|
|
1789
|
+
});
|
|
1790
|
+
try {
|
|
1791
|
+
await telegram.sendToTopic(topicId, applyResult.reply);
|
|
1792
|
+
}
|
|
1793
|
+
catch { /* @silent-fallback-ok: best-effort disclosure send (TOPIC-PROFILE-SPEC §10.1) */ }
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1690
1796
|
if (forwarded)
|
|
1691
1797
|
return false;
|
|
1692
1798
|
const topicKey = String(topicId);
|
|
@@ -1833,6 +1939,28 @@ async function handleProfileConfirm(telegram, surface, slots, topicId, principal
|
|
|
1833
1939
|
await send('That switch is no longer pending.');
|
|
1834
1940
|
return true;
|
|
1835
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Resolve the hub-intent classifier deps (Conversion #3,
|
|
1944
|
+
* docs/specs/keyword-intent-conversions-1-and-3.md) from live config + the shared
|
|
1945
|
+
* IntelligenceProvider. `enabled` is DEV-GATED (resolveDevAgentGate): DARK on the
|
|
1946
|
+
* fleet, LIVE on a development agent. `dryRun` defaults true (the soak) so a
|
|
1947
|
+
* dev-agent classifier RUNS but the message ALWAYS passes through until a
|
|
1948
|
+
* deliberate `dryRun:false`. Passed as a late-bound getter so it re-reads config
|
|
1949
|
+
* at message time.
|
|
1950
|
+
*/
|
|
1951
|
+
export function resolveHubClassifierDeps(config, intelligence) {
|
|
1952
|
+
const cfg = config.threadline?.hubIntent;
|
|
1953
|
+
return {
|
|
1954
|
+
intelligence,
|
|
1955
|
+
enabled: resolveDevAgentGate(cfg?.enabled, config),
|
|
1956
|
+
dryRun: cfg?.dryRun ?? true,
|
|
1957
|
+
minConfidence: cfg?.minConfidence ?? 0.85,
|
|
1958
|
+
timeoutMs: cfg?.timeoutMs ?? 4000,
|
|
1959
|
+
contextTurns: cfg?.contextWindowTurns ?? 6,
|
|
1960
|
+
modelTier: cfg?.modelTier ?? 'fast',
|
|
1961
|
+
stateDir: config.stateDir,
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1836
1964
|
export function wireTelegramRouting(telegram, sessionManager, quotaTracker, topicMemory, userManager, fixCommandHandler,
|
|
1837
1965
|
// Late-bound: the threadline hub deps are constructed AFTER this is wired, so
|
|
1838
1966
|
// resolve them at message-time (CMT-529 deterministic "open this" intercept).
|
|
@@ -1846,7 +1974,15 @@ getTopicOperatorStore,
|
|
|
1846
1974
|
// fix-command gate (below) only fires in that topic; everywhere else a
|
|
1847
1975
|
// message starting with restart/fix/clean is normal conversation and must
|
|
1848
1976
|
// route to the session instead of being swallowed.
|
|
1849
|
-
getAttentionTopicId
|
|
1977
|
+
getAttentionTopicId,
|
|
1978
|
+
// Late-bound (same reason as getHubDeps): the hub-intent classifier deps —
|
|
1979
|
+
// the config-resolved dev-gate/settings + the shared IntelligenceProvider.
|
|
1980
|
+
// Conversion #3 (docs/specs/keyword-intent-conversions-1-and-3.md): the
|
|
1981
|
+
// "open this"/"tie this to <topic>" DECISION is inferred by an LLM over the
|
|
1982
|
+
// message + recent conversation (HubIntentClassifier), NOT a regex — the
|
|
1983
|
+
// regex SWALLOWED the message before the agent saw it, so a misread ATE it.
|
|
1984
|
+
// Resolved at the callsite where config + sharedIntelligence are in scope.
|
|
1985
|
+
getHubClassifierDeps) {
|
|
1850
1986
|
// Guard: tracks which topic IDs have a spawn in progress.
|
|
1851
1987
|
// Prevents duplicate concurrent spawns for the same topic when messages
|
|
1852
1988
|
// arrive faster than the async spawn completes.
|
|
@@ -1942,10 +2078,19 @@ getAttentionTopicId) {
|
|
|
1942
2078
|
return;
|
|
1943
2079
|
}
|
|
1944
2080
|
// ── Threadline hub commands: "open this" / "tie this to <topic>" (CMT-529) ──
|
|
1945
|
-
// When a message in the Threadline HUB topic is a
|
|
1946
|
-
//
|
|
1947
|
-
//
|
|
2081
|
+
// When a message in the Threadline HUB topic is a bind command, bind the
|
|
2082
|
+
// conversation to a topic STRUCTURALLY — before any agent interprets it. This
|
|
2083
|
+
// onTopicMessage seam is the convergence both inbound paths reach
|
|
1948
2084
|
// (lifeline-forward AND server-polling), so one intercept covers both modes.
|
|
2085
|
+
//
|
|
2086
|
+
// Conversion #3 (docs/specs/keyword-intent-conversions-1-and-3.md): the
|
|
2087
|
+
// "is this a bind command?" DECISION is inferred by an LLM over the message +
|
|
2088
|
+
// recent conversation (HubIntentClassifier), NOT a regex. The old regex
|
|
2089
|
+
// SWALLOWED the message before the agent saw it, so a misread silently ATE a
|
|
2090
|
+
// real message. Dev-gated dark (resolveDevAgentGate at the callsite) +
|
|
2091
|
+
// dry-run FIRST: while dark or dry-run the message ALWAYS passes through
|
|
2092
|
+
// (never swallowed); the classifier still runs on a dev agent and LOGS
|
|
2093
|
+
// would-swallow vs would-pass for the soak. Fail-OPEN on any uncertainty.
|
|
1949
2094
|
// FAIL-OPEN: any error falls through to normal routing.
|
|
1950
2095
|
const hubDeps = getHubDeps?.() ?? null;
|
|
1951
2096
|
if (hubDeps) {
|
|
@@ -1958,17 +2103,78 @@ getAttentionTopicId) {
|
|
|
1958
2103
|
hubTopicId = undefined;
|
|
1959
2104
|
}
|
|
1960
2105
|
if (hubTopicId !== undefined && Number(topicId) === Number(hubTopicId)) {
|
|
1961
|
-
const
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
2106
|
+
const clsDeps = getHubClassifierDeps?.() ?? null;
|
|
2107
|
+
// Dark gate: when the classifier isn't enabled (fleet default), never
|
|
2108
|
+
// swallow — pass the message through to the agent (no regex fallback).
|
|
2109
|
+
if (clsDeps?.enabled) {
|
|
2110
|
+
const { bindHubConversation } = await import('../threadline/hubCommands.js');
|
|
2111
|
+
const { classifyHubIntent, toHubCommand } = await import('../threadline/HubIntentClassifier.js');
|
|
2112
|
+
// The enum of bindable topics (real existing topics) — the model can
|
|
2113
|
+
// only tie to one of these (structured-output guardrail).
|
|
2114
|
+
let bindableTopics = [];
|
|
2115
|
+
try {
|
|
2116
|
+
bindableTopics = telegram.getAllTopicMappings()
|
|
2117
|
+
.map((m) => ({ topicId: m.topicId, topicName: m.topicName ?? `topic ${m.topicId}` }));
|
|
2118
|
+
}
|
|
2119
|
+
catch { /* best-effort — an empty enum simply means only "open" is possible */ }
|
|
2120
|
+
// Bounded window of recent turns so context-dependent commands ("yes,
|
|
2121
|
+
// tie it to that one") can resolve their target. Best-effort.
|
|
2122
|
+
let conversationContext = [];
|
|
2123
|
+
try {
|
|
2124
|
+
const hist = telegram.getTopicHistory(topicId, clsDeps.contextTurns) ?? [];
|
|
2125
|
+
conversationContext = hist.map((h) => ({ fromUser: !!h.fromUser, text: String(h.text ?? '') }));
|
|
2126
|
+
}
|
|
2127
|
+
catch { /* context is best-effort; classify on the message alone */ }
|
|
2128
|
+
const result = await classifyHubIntent({
|
|
2129
|
+
text,
|
|
2130
|
+
bindableTopics,
|
|
2131
|
+
conversationContext,
|
|
2132
|
+
intelligence: clsDeps.intelligence,
|
|
2133
|
+
minConfidence: clsDeps.minConfidence,
|
|
2134
|
+
timeoutMs: clsDeps.timeoutMs,
|
|
2135
|
+
maxContextTurns: clsDeps.contextTurns,
|
|
2136
|
+
modelTier: clsDeps.modelTier,
|
|
2137
|
+
});
|
|
2138
|
+
const willAct = result.isCommand && !clsDeps.dryRun;
|
|
2139
|
+
// Audit the soak: log LLM-engaged decisions (a prefilter-skip is the
|
|
2140
|
+
// bulk of traffic and always a would-pass — skipping it cuts volume
|
|
2141
|
+
// AND the message-preview privacy surface). Never gates the message.
|
|
2142
|
+
if (result.source !== 'prefilter-skip') {
|
|
2143
|
+
try {
|
|
2144
|
+
const logDir = path.join(clsDeps.stateDir, 'logs');
|
|
2145
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
2146
|
+
const decision = result.isCommand ? (willAct ? 'swallow' : 'would-swallow') : 'pass';
|
|
2147
|
+
fs.appendFileSync(path.join(logDir, 'hub-intent.jsonl'), JSON.stringify({
|
|
2148
|
+
ts: new Date().toISOString(),
|
|
2149
|
+
topicId,
|
|
2150
|
+
decision,
|
|
2151
|
+
dryRun: clsDeps.dryRun,
|
|
2152
|
+
source: result.source,
|
|
2153
|
+
intent: result.intent,
|
|
2154
|
+
targetTopicId: result.targetTopicId,
|
|
2155
|
+
confidence: result.confidence,
|
|
2156
|
+
reason: result.reason,
|
|
2157
|
+
textPreview: String(text).replace(/\s+/g, ' ').trim().slice(0, 80),
|
|
2158
|
+
}) + '\n');
|
|
2159
|
+
}
|
|
2160
|
+
catch { /* audit is observability — never gates the message path */ }
|
|
2161
|
+
}
|
|
2162
|
+
// Dry-run (the soak) OR not-a-command OR fail-open → pass the message
|
|
2163
|
+
// through untouched. Real swallowing needs a deliberate dryRun:false,
|
|
2164
|
+
// AND fail-open means uncertainty never swallows.
|
|
2165
|
+
if (willAct) {
|
|
2166
|
+
const cmd = toHubCommand(result);
|
|
2167
|
+
if (cmd) {
|
|
2168
|
+
const bindResult = await bindHubConversation(hubDeps, { ...cmd, autoPick: true }); // human "open this" → auto-pick most-recent, never 409
|
|
2169
|
+
if (!bindResult.ok) {
|
|
2170
|
+
await telegram.sendToTopic(topicId, bindResult.status === 404
|
|
2171
|
+
? 'Nothing waiting in the hub to open right now.'
|
|
2172
|
+
: `Couldn't open that — ${bindResult.error}`).catch(() => { });
|
|
2173
|
+
}
|
|
2174
|
+
// On success bindHubConversation already posted the hub confirmation.
|
|
2175
|
+
return; // structural: never inject the command into a session
|
|
2176
|
+
}
|
|
1969
2177
|
}
|
|
1970
|
-
// On success bindHubConversation already posted the hub confirmation.
|
|
1971
|
-
return; // structural: never inject the command into a session
|
|
1972
2178
|
}
|
|
1973
2179
|
}
|
|
1974
2180
|
}
|
|
@@ -5229,6 +5435,19 @@ export async function startServer(options) {
|
|
|
5229
5435
|
const { TopicProfileResolver } = await import('../core/TopicProfileResolver.js');
|
|
5230
5436
|
const { normalizeTierEscalationConfig } = await import('../core/ModelTierEscalation.js');
|
|
5231
5437
|
const topicProfilesCfg = config.topicProfiles;
|
|
5438
|
+
// Offender #1 conversion: resolve the ProfileIntentClassifier settings ONCE
|
|
5439
|
+
// (the module-level ingress handler has no config in scope). `enabled` rides
|
|
5440
|
+
// resolveDevAgentGate (dark on the fleet, live on a dev agent — registered in
|
|
5441
|
+
// DEV_GATED_FEATURES, configPath topicProfiles.intentClassifier.enabled);
|
|
5442
|
+
// dryRun ships true (the dry-run canary — actuates only on a deliberate flip).
|
|
5443
|
+
const _intentCfg = topicProfilesCfg?.intentClassifier;
|
|
5444
|
+
_profileIntentEnabled = resolveDevAgentGate(_intentCfg?.enabled, config);
|
|
5445
|
+
_profileIntentDryRun = _intentCfg?.dryRun ?? true;
|
|
5446
|
+
_profileIntentMinConfidence = _intentCfg?.minConfidence ?? 0.85;
|
|
5447
|
+
_profileIntentTimeoutMs = _intentCfg?.timeoutMs ?? 4000;
|
|
5448
|
+
_profileIntentContextTurns = _intentCfg?.contextWindowTurns ?? 6;
|
|
5449
|
+
_profileIntentModelTier = _intentCfg?.modelTier ?? 'fast';
|
|
5450
|
+
_profileIntentStateDir = config.stateDir;
|
|
5232
5451
|
_topicProfileStore = new TopicProfileStore({
|
|
5233
5452
|
stateFilePath: path.join(config.stateDir, 'state', 'topic-profiles.json'),
|
|
5234
5453
|
legacyFrameworksPath: path.join(config.stateDir, 'state', 'topic-frameworks.json'),
|
|
@@ -6198,7 +6417,7 @@ export async function startServer(options) {
|
|
|
6198
6417
|
const userManagerSendOnly = new UserManager(config.stateDir, config.users);
|
|
6199
6418
|
attachUserReplication(userManagerSendOnly); // WS2.6 send-side (dark by default)
|
|
6200
6419
|
_fixDeps = { state, liveConfig, sessionManager, telegram, config };
|
|
6201
|
-
wireTelegramRouting(telegram, sessionManager, quotaTracker, topicMemory, userManagerSendOnly, (topicId, text) => handleFixCommand(topicId, text, _fixDeps), () => (collaborationSurfacer && conversationStore && telegram) ? { collaborationSurfacer, conversationStore, commitmentTracker, telegram, brief: briefDeps } : null, () => _agentServerRef?.getTopicOperatorStore() ?? null, () => state.get('agent-attention-topic'));
|
|
6420
|
+
wireTelegramRouting(telegram, sessionManager, quotaTracker, topicMemory, userManagerSendOnly, (topicId, text) => handleFixCommand(topicId, text, _fixDeps), () => (collaborationSurfacer && conversationStore && telegram) ? { collaborationSurfacer, conversationStore, commitmentTracker, telegram, brief: briefDeps } : null, () => _agentServerRef?.getTopicOperatorStore() ?? null, () => state.get('agent-attention-topic'), () => resolveHubClassifierDeps(config, _sharedIntelligence));
|
|
6202
6421
|
wireTelegramCallbacks(telegram, sessionManager, state, quotaTracker, undefined, config.sessions.claudePath, topicMemory);
|
|
6203
6422
|
console.log(pc.green(' Telegram routing + command callbacks wired (send-only)'));
|
|
6204
6423
|
}
|
|
@@ -6330,7 +6549,7 @@ export async function startServer(options) {
|
|
|
6330
6549
|
config,
|
|
6331
6550
|
};
|
|
6332
6551
|
// Wire up topic → session routing and session management callbacks
|
|
6333
|
-
wireTelegramRouting(telegram, sessionManager, quotaTracker, topicMemory, userManager, (topicId, text) => handleFixCommand(topicId, text, _fixDeps), () => (collaborationSurfacer && conversationStore && telegram) ? { collaborationSurfacer, conversationStore, commitmentTracker, telegram, brief: briefDeps } : null, () => _agentServerRef?.getTopicOperatorStore() ?? null, () => state.get('agent-attention-topic'));
|
|
6552
|
+
wireTelegramRouting(telegram, sessionManager, quotaTracker, topicMemory, userManager, (topicId, text) => handleFixCommand(topicId, text, _fixDeps), () => (collaborationSurfacer && conversationStore && telegram) ? { collaborationSurfacer, conversationStore, commitmentTracker, telegram, brief: briefDeps } : null, () => _agentServerRef?.getTopicOperatorStore() ?? null, () => state.get('agent-attention-topic'), () => resolveHubClassifierDeps(config, _sharedIntelligence));
|
|
6334
6553
|
wireTelegramCallbacks(telegram, sessionManager, state, quotaTracker, accountSwitcher, config.sessions.claudePath, topicMemory);
|
|
6335
6554
|
// Wire up unknown-user handling (Multi-User Setup Wizard Phase 4.5)
|
|
6336
6555
|
telegram.onGetRegistrationPolicy = () => ({
|