robrain 2.4.9 → 2.4.11
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/check.js +2 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/export-memory.js +2 -2
- package/dist/commands/export-memory.js.map +1 -1
- package/dist/commands/init-project.js +4 -4
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/inject.d.ts +10 -0
- package/dist/commands/inject.d.ts.map +1 -1
- package/dist/commands/inject.js +52 -6
- package/dist/commands/inject.js.map +1 -1
- package/dist/commands/inject.test.d.ts +2 -0
- package/dist/commands/inject.test.d.ts.map +1 -0
- package/dist/commands/inject.test.js +170 -0
- package/dist/commands/inject.test.js.map +1 -0
- package/dist/commands/install.js +5 -5
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/outcomes.js +4 -4
- package/dist/commands/outcomes.js.map +1 -1
- package/dist/commands/review.js +7 -7
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +45 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/config.test.d.ts +2 -0
- package/dist/lib/config.test.d.ts.map +1 -0
- package/dist/lib/config.test.js +75 -0
- package/dist/lib/config.test.js.map +1 -0
- package/dist/lib/editor.d.ts.map +1 -1
- package/dist/lib/editor.js +2 -0
- package/dist/lib/editor.js.map +1 -1
- package/dist/lib/spinner.d.ts +27 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +41 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/lib/spinner.test.d.ts +2 -0
- package/dist/lib/spinner.test.d.ts.map +1 -0
- package/dist/lib/spinner.test.js +39 -0
- package/dist/lib/spinner.test.js.map +1 -0
- package/package.json +2 -2
- package/vendor/sensing-mcp/dist/index.js +128 -48
- package/vendor/sensing-mcp/package.json +1 -1
- package/vendor/synthesis/dist/index.js +48 -14
- package/vendor/synthesis/package.json +1 -1
|
@@ -6440,6 +6440,21 @@ var require_llm = __commonJS({
|
|
|
6440
6440
|
}
|
|
6441
6441
|
var OPENAI_MAX_ATTEMPTS = 4;
|
|
6442
6442
|
var OPENAI_BASE_DELAY_MS = 400;
|
|
6443
|
+
function waitBeforeRetry(attempt, signal) {
|
|
6444
|
+
signal?.throwIfAborted();
|
|
6445
|
+
const delayMs = OPENAI_BASE_DELAY_MS * 2 ** attempt + Math.floor(Math.random() * 150);
|
|
6446
|
+
return new Promise((resolve, reject) => {
|
|
6447
|
+
const onAbort = () => {
|
|
6448
|
+
clearTimeout(timer);
|
|
6449
|
+
reject(signal?.reason);
|
|
6450
|
+
};
|
|
6451
|
+
const timer = setTimeout(() => {
|
|
6452
|
+
signal?.removeEventListener("abort", onAbort);
|
|
6453
|
+
resolve();
|
|
6454
|
+
}, delayMs);
|
|
6455
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
6456
|
+
});
|
|
6457
|
+
}
|
|
6443
6458
|
async function openaiChat(params) {
|
|
6444
6459
|
const baseUrl = params.baseUrl ?? resolveOpenAiBaseUrl2();
|
|
6445
6460
|
if (!params.apiKey && baseUrl === exports.DEFAULT_OPENAI_BASE_URL) {
|
|
@@ -6463,10 +6478,16 @@ var require_llm = __commonJS({
|
|
|
6463
6478
|
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
6464
6479
|
method: "POST",
|
|
6465
6480
|
headers,
|
|
6466
|
-
body
|
|
6481
|
+
body,
|
|
6482
|
+
signal: params.signal
|
|
6467
6483
|
});
|
|
6468
6484
|
if (res.ok) {
|
|
6469
6485
|
const data = await res.json();
|
|
6486
|
+
params.onUsage?.({
|
|
6487
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
6488
|
+
outputTokens: data.usage?.completion_tokens ?? 0,
|
|
6489
|
+
attempts: attempt + 1
|
|
6490
|
+
});
|
|
6470
6491
|
return data.choices?.[0]?.message?.content ?? "";
|
|
6471
6492
|
}
|
|
6472
6493
|
lastErr = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
@@ -6474,8 +6495,7 @@ var require_llm = __commonJS({
|
|
|
6474
6495
|
if (!retriable || attempt >= OPENAI_MAX_ATTEMPTS - 1) {
|
|
6475
6496
|
throw new Error(`OpenAI chat failed: ${lastErr}`);
|
|
6476
6497
|
}
|
|
6477
|
-
|
|
6478
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
6498
|
+
await waitBeforeRetry(attempt, params.signal);
|
|
6479
6499
|
}
|
|
6480
6500
|
throw new Error(`OpenAI chat failed: ${lastErr}`);
|
|
6481
6501
|
}
|
|
@@ -6499,10 +6519,16 @@ var require_llm = __commonJS({
|
|
|
6499
6519
|
"anthropic-version": "2023-06-01",
|
|
6500
6520
|
"Content-Type": "application/json"
|
|
6501
6521
|
},
|
|
6502
|
-
body
|
|
6522
|
+
body,
|
|
6523
|
+
signal: params.signal
|
|
6503
6524
|
});
|
|
6504
6525
|
if (res.ok) {
|
|
6505
6526
|
const data = await res.json();
|
|
6527
|
+
params.onUsage?.({
|
|
6528
|
+
inputTokens: data.usage?.input_tokens ?? 0,
|
|
6529
|
+
outputTokens: data.usage?.output_tokens ?? 0,
|
|
6530
|
+
attempts: attempt + 1
|
|
6531
|
+
});
|
|
6506
6532
|
const block = data.content?.[0];
|
|
6507
6533
|
return block?.type === "text" ? block.text ?? "" : "";
|
|
6508
6534
|
}
|
|
@@ -6511,8 +6537,7 @@ var require_llm = __commonJS({
|
|
|
6511
6537
|
if (!retriable || attempt >= OPENAI_MAX_ATTEMPTS - 1) {
|
|
6512
6538
|
throw new Error(`Anthropic chat failed: ${lastErr}`);
|
|
6513
6539
|
}
|
|
6514
|
-
|
|
6515
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
6540
|
+
await waitBeforeRetry(attempt, params.signal);
|
|
6516
6541
|
}
|
|
6517
6542
|
throw new Error(`Anthropic chat failed: ${lastErr}`);
|
|
6518
6543
|
}
|
|
@@ -7170,7 +7195,7 @@ var require_extract_decision = __commonJS({
|
|
|
7170
7195
|
"../shared/dist/extract-decision.js"(exports) {
|
|
7171
7196
|
"use strict";
|
|
7172
7197
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7173
|
-
exports.LlmKeyMissingError = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
|
|
7198
|
+
exports.LlmKeyMissingError = exports.SELF_HOSTED_USER_RULE_OVERRIDE = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
|
|
7174
7199
|
exports.stripMarkdownJsonFence = stripMarkdownJsonFence;
|
|
7175
7200
|
exports.parseExtractedDecision = parseExtractedDecision;
|
|
7176
7201
|
exports.extractDecisionLlm = extractDecisionLlm2;
|
|
@@ -7200,6 +7225,9 @@ Fields:
|
|
|
7200
7225
|
Output ONLY valid JSON. If no decision: {"decision": null, "rationale": null, "rejected": [], "confidence": 0}.
|
|
7201
7226
|
Never add explanation outside the JSON.
|
|
7202
7227
|
Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number}`;
|
|
7228
|
+
exports.SELF_HOSTED_USER_RULE_OVERRIDE = `User-stated rule wins:
|
|
7229
|
+
- A durable rule is a standing instruction for future work in this repo or team (e.g. "from now on", "always", "never", "we no longer"). An instruction scoped to the current task, file, or step (e.g. "don't use that file", "stop logging here", "don't add tests for this one") is not durable; apply the rules above to it.
|
|
7230
|
+
- If the user states a durable rule in this turn, extract that rule even when the assistant refuses it, cites an older rule, or says it will not follow the new one. In that case do not return null, and do not extract the older rule the assistant restates.`;
|
|
7203
7231
|
var LlmKeyMissingError2 = class extends Error {
|
|
7204
7232
|
constructor(message) {
|
|
7205
7233
|
super(message);
|
|
@@ -7207,10 +7235,9 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
|
|
|
7207
7235
|
}
|
|
7208
7236
|
};
|
|
7209
7237
|
exports.LlmKeyMissingError = LlmKeyMissingError2;
|
|
7238
|
+
var EXTRACTION_MAX_TOKENS = 600;
|
|
7210
7239
|
function stripMarkdownJsonFence(raw) {
|
|
7211
|
-
|
|
7212
|
-
const m = t.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
7213
|
-
return m?.[1]?.trim() ?? t;
|
|
7240
|
+
return raw.trim().replace(/^```[a-z]*\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
7214
7241
|
}
|
|
7215
7242
|
function parseExtractedDecision(rawText) {
|
|
7216
7243
|
const raw = JSON.parse(stripMarkdownJsonFence(rawText));
|
|
@@ -7227,6 +7254,9 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
|
|
|
7227
7254
|
const userPrompt = `Session turn:
|
|
7228
7255
|
User: ${userMessage}
|
|
7229
7256
|
Claude: ${claudeReply}`;
|
|
7257
|
+
const systemPrompt = cfg.extraSystemRules ? `${exports.DECISION_EXTRACTION_SYSTEM_PROMPT}
|
|
7258
|
+
|
|
7259
|
+
${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
|
|
7230
7260
|
let rawText;
|
|
7231
7261
|
if (cfg.provider === "openai") {
|
|
7232
7262
|
const baseUrl = cfg.openaiBaseUrl ?? (0, llm_js_1.resolveOpenAiBaseUrl)();
|
|
@@ -7236,9 +7266,9 @@ Claude: ${claudeReply}`;
|
|
|
7236
7266
|
rawText = await (0, llm_js_1.openaiChat)({
|
|
7237
7267
|
apiKey: cfg.openaiApiKey ?? "",
|
|
7238
7268
|
model: cfg.openaiModel,
|
|
7239
|
-
system:
|
|
7269
|
+
system: systemPrompt,
|
|
7240
7270
|
user: userPrompt,
|
|
7241
|
-
maxTokens:
|
|
7271
|
+
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
7242
7272
|
baseUrl,
|
|
7243
7273
|
json: true
|
|
7244
7274
|
});
|
|
@@ -7249,9 +7279,13 @@ Claude: ${claudeReply}`;
|
|
|
7249
7279
|
rawText = await (0, llm_js_1.anthropicChat)({
|
|
7250
7280
|
apiKey: cfg.anthropicApiKey.trim(),
|
|
7251
7281
|
model: cfg.anthropicModel,
|
|
7252
|
-
system:
|
|
7282
|
+
system: systemPrompt,
|
|
7253
7283
|
user: userPrompt,
|
|
7254
|
-
maxTokens:
|
|
7284
|
+
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
7285
|
+
// Same turn, same extraction: the Anthropic API samples at temperature 1
|
|
7286
|
+
// unless told otherwise, which made a clear decision come back as
|
|
7287
|
+
// "no decision" on some runs and get extracted on others.
|
|
7288
|
+
temperature: 0
|
|
7255
7289
|
});
|
|
7256
7290
|
}
|
|
7257
7291
|
return parseExtractedDecision(rawText);
|
|
@@ -22899,7 +22933,8 @@ async function extractDecision(turn) {
|
|
|
22899
22933
|
anthropicModel: config2.anthropicModel,
|
|
22900
22934
|
openaiApiKey: config2.openaiApiKey,
|
|
22901
22935
|
openaiModel: config2.openaiLlmModel,
|
|
22902
|
-
openaiBaseUrl: config2.openaiBaseUrl
|
|
22936
|
+
openaiBaseUrl: config2.openaiBaseUrl,
|
|
22937
|
+
extraSystemRules: import_shared4.SELF_HOSTED_USER_RULE_OVERRIDE
|
|
22903
22938
|
});
|
|
22904
22939
|
lastClassifierFailure = null;
|
|
22905
22940
|
return extracted;
|
|
@@ -23035,7 +23070,10 @@ var init_classifiers = __esm({
|
|
|
23035
23070
|
/npm install|pnpm add|yarn add/
|
|
23036
23071
|
];
|
|
23037
23072
|
IMPLICIT_USER_COMMITMENT_PATTERNS = [
|
|
23038
|
-
/\blet['’]s\s+(standardize|standardise|use|go with|stick with|adopt)\b/i
|
|
23073
|
+
/\blet['’]s\s+(standardize|standardise|use|go with|stick with|adopt)\b/i,
|
|
23074
|
+
/\b(from now on|going forward)\b/i,
|
|
23075
|
+
/\b(always|never)\s+(use|write|add|call|prefer|put|name|run|import|suggest|commit|push|merge|install|mock)\b/i,
|
|
23076
|
+
/\b(don['’]t|do not|stop|no longer)\s+(use|using|write|writing|add|adding|call|calling|suggest|suggesting|import|importing)\b/i
|
|
23039
23077
|
];
|
|
23040
23078
|
embeddingWindows = /* @__PURE__ */ new Map();
|
|
23041
23079
|
recentFilesPerSession = /* @__PURE__ */ new Map();
|
|
@@ -23100,8 +23138,9 @@ async function routeDecisionSignal(signal, projectId) {
|
|
|
23100
23138
|
userFacing: soft || "Perception did not persist this signal (discarded or below threshold)."
|
|
23101
23139
|
};
|
|
23102
23140
|
}
|
|
23103
|
-
if (payload.action === "written" || payload.action === "deduped") {
|
|
23104
|
-
|
|
23141
|
+
if (payload.action === "written" || payload.action === "deduped" || payload.action === "conflict_flagged" || payload.action === "auto_resolved_supersession" || payload.action === "quarantined") {
|
|
23142
|
+
const notice = payload.conflict_notice?.trim();
|
|
23143
|
+
return { persisted: true, ...notice ? { conflictNotice: notice } : {} };
|
|
23105
23144
|
}
|
|
23106
23145
|
const softFallback = [payload.message].filter(Boolean).join(" ");
|
|
23107
23146
|
console.error(
|
|
@@ -23187,6 +23226,25 @@ var init_router = __esm({
|
|
|
23187
23226
|
}
|
|
23188
23227
|
});
|
|
23189
23228
|
|
|
23229
|
+
// src/pending-notices.ts
|
|
23230
|
+
function rememberConflictNotice(notices, sessionId, notice, maxSessions = MAX_PENDING_CONFLICT_NOTICES) {
|
|
23231
|
+
const held = notices.get(sessionId) ?? [];
|
|
23232
|
+
const alreadyHeld = notices.has(sessionId);
|
|
23233
|
+
if (!held.includes(notice)) held.push(notice);
|
|
23234
|
+
if (!alreadyHeld && notices.size >= maxSessions) {
|
|
23235
|
+
const oldestSessionId = notices.keys().next().value;
|
|
23236
|
+
if (oldestSessionId !== void 0) notices.delete(oldestSessionId);
|
|
23237
|
+
}
|
|
23238
|
+
notices.set(sessionId, held);
|
|
23239
|
+
}
|
|
23240
|
+
var MAX_PENDING_CONFLICT_NOTICES;
|
|
23241
|
+
var init_pending_notices = __esm({
|
|
23242
|
+
"src/pending-notices.ts"() {
|
|
23243
|
+
"use strict";
|
|
23244
|
+
MAX_PENDING_CONFLICT_NOTICES = 1e3;
|
|
23245
|
+
}
|
|
23246
|
+
});
|
|
23247
|
+
|
|
23190
23248
|
// src/session-registry.ts
|
|
23191
23249
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23192
23250
|
import { dirname } from "node:path";
|
|
@@ -23286,6 +23344,15 @@ __export(server_exports, {
|
|
|
23286
23344
|
sessionRegistry: () => sessionRegistry
|
|
23287
23345
|
});
|
|
23288
23346
|
import { randomBytes } from "node:crypto";
|
|
23347
|
+
function holdConflictNotice(sessionId, notice) {
|
|
23348
|
+
if (!sessionRegistry.get(sessionId)) return;
|
|
23349
|
+
rememberConflictNotice(pendingConflictNotices, sessionId, notice, MAX_PENDING_CONFLICT_NOTICES);
|
|
23350
|
+
}
|
|
23351
|
+
function takeConflictNotice(sessionId) {
|
|
23352
|
+
const held = pendingConflictNotices.get(sessionId);
|
|
23353
|
+
pendingConflictNotices.delete(sessionId);
|
|
23354
|
+
return held?.length ? held.join(" ") : null;
|
|
23355
|
+
}
|
|
23289
23356
|
function generateSessionId() {
|
|
23290
23357
|
return `${(/* @__PURE__ */ new Date()).toISOString()}-${randomBytes(2).toString("hex")}`;
|
|
23291
23358
|
}
|
|
@@ -23297,7 +23364,7 @@ function resolveSessionId(raw) {
|
|
|
23297
23364
|
function buildServer() {
|
|
23298
23365
|
const server = new McpServer({
|
|
23299
23366
|
name: "sensing-mcp",
|
|
23300
|
-
version: "2.4.
|
|
23367
|
+
version: "2.4.11"
|
|
23301
23368
|
});
|
|
23302
23369
|
server.tool(
|
|
23303
23370
|
"sensing_start_session",
|
|
@@ -23332,9 +23399,10 @@ function buildServer() {
|
|
|
23332
23399
|
};
|
|
23333
23400
|
}
|
|
23334
23401
|
);
|
|
23402
|
+
const recordTurnDescription = "Record a completed conversation turn (user message + Claude reply). Call this after every exchange. Survives server restarts \u2014 keep using the same session_id even if the server reconnected mid-conversation. Returns whether a topic shift was detected \u2014 if true, call your context injection tool." + (isThinMode() ? "" : " A new durable rule from the user is still recorded when it conflicts with an older one. If conflict_notice is set, tell the user that sentence; it can refer to a rule from an earlier turn. Both decisions were saved. It is a heads-up, not a stop.");
|
|
23335
23403
|
server.tool(
|
|
23336
23404
|
"sensing_record_turn",
|
|
23337
|
-
|
|
23405
|
+
recordTurnDescription,
|
|
23338
23406
|
{
|
|
23339
23407
|
session_id: external_exports.string().describe("Session identifier from sensing_start_session"),
|
|
23340
23408
|
sequence: external_exports.number().int().describe("Turn number within the session, starting at 1"),
|
|
@@ -23396,31 +23464,9 @@ function buildServer() {
|
|
|
23396
23464
|
console.error("[Sensing] Topic shift classifier error:", err);
|
|
23397
23465
|
}
|
|
23398
23466
|
const projectId = session.project_id;
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
const errors = await routeFlushTurns([turn], projectId);
|
|
23403
|
-
if (errors.length === 0) {
|
|
23404
|
-
lastDecisionShipFailure = null;
|
|
23405
|
-
streamBuffer.markClassified(session_id, sequence);
|
|
23406
|
-
} else {
|
|
23407
|
-
lastDecisionShipFailure = `${session_id} seq ${sequence}: ${errors.join("; ")}`;
|
|
23408
|
-
}
|
|
23409
|
-
return;
|
|
23410
|
-
}
|
|
23411
|
-
const decisionSignal = await classifyDecision(turn, projectId);
|
|
23412
|
-
if (decisionSignal) {
|
|
23413
|
-
const outcome = await routeDecisionSignal(decisionSignal, projectId);
|
|
23414
|
-
if (outcome.persisted) {
|
|
23415
|
-
lastDecisionShipFailure = null;
|
|
23416
|
-
streamBuffer.markClassified(session_id, sequence);
|
|
23417
|
-
} else {
|
|
23418
|
-
lastDecisionShipFailure = outcome.userFacing ?? `${session_id} seq ${sequence}: Perception did not persist signal (see Sensing stderr / Perception logs)`;
|
|
23419
|
-
}
|
|
23420
|
-
}
|
|
23421
|
-
} catch (err) {
|
|
23422
|
-
console.error("[Sensing] Decision classifier error:", err);
|
|
23423
|
-
}
|
|
23467
|
+
const conflictNotice = takeConflictNotice(session_id);
|
|
23468
|
+
setImmediate(() => {
|
|
23469
|
+
void (isThinMode() ? shipThinModeTurn(turn, projectId, session_id, sequence) : shipRecordedTurn(turn, projectId, session_id, sequence));
|
|
23424
23470
|
});
|
|
23425
23471
|
if (injected_memory_ids.length > 0) {
|
|
23426
23472
|
setImmediate(async () => {
|
|
@@ -23448,7 +23494,8 @@ function buildServer() {
|
|
|
23448
23494
|
topic_shift: topicShift,
|
|
23449
23495
|
task_description: taskDescription,
|
|
23450
23496
|
secrets_redacted: secretsRedacted,
|
|
23451
|
-
sequence
|
|
23497
|
+
sequence,
|
|
23498
|
+
...conflictNotice ? { conflict_notice: conflictNotice } : {}
|
|
23452
23499
|
})
|
|
23453
23500
|
}]
|
|
23454
23501
|
};
|
|
@@ -23481,6 +23528,7 @@ function buildServer() {
|
|
|
23481
23528
|
}).catch((err) => console.error("[Sensing] Flush-on-close failed:", err));
|
|
23482
23529
|
sessionRegistry.remove(session_id);
|
|
23483
23530
|
clearSessionEmbeddings(session_id);
|
|
23531
|
+
const conflictNotice = takeConflictNotice(session_id);
|
|
23484
23532
|
console.error(`[Sensing] Session ended: ${session_id} \u2014 flushing ${flushed} turn(s) in background`);
|
|
23485
23533
|
return {
|
|
23486
23534
|
content: [{
|
|
@@ -23489,7 +23537,8 @@ function buildServer() {
|
|
|
23489
23537
|
session_id,
|
|
23490
23538
|
flushed,
|
|
23491
23539
|
pending: flushed,
|
|
23492
|
-
summary: summary ?? null
|
|
23540
|
+
summary: summary ?? null,
|
|
23541
|
+
...conflictNotice ? { conflict_notice: conflictNotice } : {}
|
|
23493
23542
|
})
|
|
23494
23543
|
}]
|
|
23495
23544
|
};
|
|
@@ -23528,6 +23577,35 @@ function buildServer() {
|
|
|
23528
23577
|
);
|
|
23529
23578
|
return server;
|
|
23530
23579
|
}
|
|
23580
|
+
async function shipThinModeTurn(turn, projectId, sessionId, sequence) {
|
|
23581
|
+
try {
|
|
23582
|
+
const errors = await routeFlushTurns([turn], projectId);
|
|
23583
|
+
if (errors.length === 0) {
|
|
23584
|
+
lastDecisionShipFailure = null;
|
|
23585
|
+
streamBuffer.markClassified(sessionId, sequence);
|
|
23586
|
+
} else {
|
|
23587
|
+
lastDecisionShipFailure = `${sessionId} seq ${sequence}: ${errors.join("; ")}`;
|
|
23588
|
+
}
|
|
23589
|
+
} catch (err) {
|
|
23590
|
+
console.error("[Sensing] Decision classifier error:", err);
|
|
23591
|
+
}
|
|
23592
|
+
}
|
|
23593
|
+
async function shipRecordedTurn(turn, projectId, sessionId, sequence) {
|
|
23594
|
+
try {
|
|
23595
|
+
const decisionSignal = await classifyDecision(turn, projectId);
|
|
23596
|
+
if (!decisionSignal) return;
|
|
23597
|
+
const outcome = await routeDecisionSignal(decisionSignal, projectId);
|
|
23598
|
+
if (outcome.persisted) {
|
|
23599
|
+
lastDecisionShipFailure = null;
|
|
23600
|
+
streamBuffer.markClassified(sessionId, sequence);
|
|
23601
|
+
if (outcome.conflictNotice) holdConflictNotice(sessionId, outcome.conflictNotice);
|
|
23602
|
+
return;
|
|
23603
|
+
}
|
|
23604
|
+
lastDecisionShipFailure = outcome.userFacing ?? `${sessionId} seq ${sequence}: Perception did not persist signal (see Sensing stderr / Perception logs)`;
|
|
23605
|
+
} catch (err) {
|
|
23606
|
+
console.error("[Sensing] Decision classifier error:", err);
|
|
23607
|
+
}
|
|
23608
|
+
}
|
|
23531
23609
|
function withTimeout(promise, ms, fallback) {
|
|
23532
23610
|
promise.catch((err) => {
|
|
23533
23611
|
console.error("[Sensing] Suppressed late rejection after timeout:", err);
|
|
@@ -23572,7 +23650,7 @@ async function fetchAlwaysOnSummary(projectId) {
|
|
|
23572
23650
|
return { text: `Project: ${projectId}` };
|
|
23573
23651
|
}
|
|
23574
23652
|
}
|
|
23575
|
-
var import_shared5, sessionRegistry, lastDecisionShipFailure;
|
|
23653
|
+
var import_shared5, sessionRegistry, lastDecisionShipFailure, pendingConflictNotices;
|
|
23576
23654
|
var init_server3 = __esm({
|
|
23577
23655
|
"src/server.ts"() {
|
|
23578
23656
|
"use strict";
|
|
@@ -23583,9 +23661,11 @@ var init_server3 = __esm({
|
|
|
23583
23661
|
init_classifiers();
|
|
23584
23662
|
init_router();
|
|
23585
23663
|
init_config();
|
|
23664
|
+
init_pending_notices();
|
|
23586
23665
|
init_session_registry();
|
|
23587
23666
|
sessionRegistry = new SessionRegistry(config2.sessionRegistryPath);
|
|
23588
23667
|
lastDecisionShipFailure = null;
|
|
23668
|
+
pendingConflictNotices = /* @__PURE__ */ new Map();
|
|
23589
23669
|
}
|
|
23590
23670
|
});
|
|
23591
23671
|
|
|
@@ -11704,6 +11704,21 @@ var require_llm = __commonJS({
|
|
|
11704
11704
|
}
|
|
11705
11705
|
var OPENAI_MAX_ATTEMPTS = 4;
|
|
11706
11706
|
var OPENAI_BASE_DELAY_MS = 400;
|
|
11707
|
+
function waitBeforeRetry(attempt, signal) {
|
|
11708
|
+
signal?.throwIfAborted();
|
|
11709
|
+
const delayMs = OPENAI_BASE_DELAY_MS * 2 ** attempt + Math.floor(Math.random() * 150);
|
|
11710
|
+
return new Promise((resolve, reject) => {
|
|
11711
|
+
const onAbort = () => {
|
|
11712
|
+
clearTimeout(timer);
|
|
11713
|
+
reject(signal?.reason);
|
|
11714
|
+
};
|
|
11715
|
+
const timer = setTimeout(() => {
|
|
11716
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11717
|
+
resolve();
|
|
11718
|
+
}, delayMs);
|
|
11719
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11720
|
+
});
|
|
11721
|
+
}
|
|
11707
11722
|
async function openaiChat2(params) {
|
|
11708
11723
|
const baseUrl = params.baseUrl ?? resolveOpenAiBaseUrl2();
|
|
11709
11724
|
if (!params.apiKey && baseUrl === exports.DEFAULT_OPENAI_BASE_URL) {
|
|
@@ -11727,10 +11742,16 @@ var require_llm = __commonJS({
|
|
|
11727
11742
|
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
11728
11743
|
method: "POST",
|
|
11729
11744
|
headers,
|
|
11730
|
-
body
|
|
11745
|
+
body,
|
|
11746
|
+
signal: params.signal
|
|
11731
11747
|
});
|
|
11732
11748
|
if (res.ok) {
|
|
11733
11749
|
const data = await res.json();
|
|
11750
|
+
params.onUsage?.({
|
|
11751
|
+
inputTokens: data.usage?.prompt_tokens ?? 0,
|
|
11752
|
+
outputTokens: data.usage?.completion_tokens ?? 0,
|
|
11753
|
+
attempts: attempt + 1
|
|
11754
|
+
});
|
|
11734
11755
|
return data.choices?.[0]?.message?.content ?? "";
|
|
11735
11756
|
}
|
|
11736
11757
|
lastErr = `${res.status}${res.statusText ? ` ${res.statusText}` : ""}`;
|
|
@@ -11738,8 +11759,7 @@ var require_llm = __commonJS({
|
|
|
11738
11759
|
if (!retriable || attempt >= OPENAI_MAX_ATTEMPTS - 1) {
|
|
11739
11760
|
throw new Error(`OpenAI chat failed: ${lastErr}`);
|
|
11740
11761
|
}
|
|
11741
|
-
|
|
11742
|
-
await new Promise((r2) => setTimeout(r2, delay));
|
|
11762
|
+
await waitBeforeRetry(attempt, params.signal);
|
|
11743
11763
|
}
|
|
11744
11764
|
throw new Error(`OpenAI chat failed: ${lastErr}`);
|
|
11745
11765
|
}
|
|
@@ -11763,10 +11783,16 @@ var require_llm = __commonJS({
|
|
|
11763
11783
|
"anthropic-version": "2023-06-01",
|
|
11764
11784
|
"Content-Type": "application/json"
|
|
11765
11785
|
},
|
|
11766
|
-
body
|
|
11786
|
+
body,
|
|
11787
|
+
signal: params.signal
|
|
11767
11788
|
});
|
|
11768
11789
|
if (res.ok) {
|
|
11769
11790
|
const data = await res.json();
|
|
11791
|
+
params.onUsage?.({
|
|
11792
|
+
inputTokens: data.usage?.input_tokens ?? 0,
|
|
11793
|
+
outputTokens: data.usage?.output_tokens ?? 0,
|
|
11794
|
+
attempts: attempt + 1
|
|
11795
|
+
});
|
|
11770
11796
|
const block = data.content?.[0];
|
|
11771
11797
|
return block?.type === "text" ? block.text ?? "" : "";
|
|
11772
11798
|
}
|
|
@@ -11775,8 +11801,7 @@ var require_llm = __commonJS({
|
|
|
11775
11801
|
if (!retriable || attempt >= OPENAI_MAX_ATTEMPTS - 1) {
|
|
11776
11802
|
throw new Error(`Anthropic chat failed: ${lastErr}`);
|
|
11777
11803
|
}
|
|
11778
|
-
|
|
11779
|
-
await new Promise((r2) => setTimeout(r2, delay));
|
|
11804
|
+
await waitBeforeRetry(attempt, params.signal);
|
|
11780
11805
|
}
|
|
11781
11806
|
throw new Error(`Anthropic chat failed: ${lastErr}`);
|
|
11782
11807
|
}
|
|
@@ -12434,7 +12459,7 @@ var require_extract_decision = __commonJS({
|
|
|
12434
12459
|
"../shared/dist/extract-decision.js"(exports) {
|
|
12435
12460
|
"use strict";
|
|
12436
12461
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12437
|
-
exports.LlmKeyMissingError = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
|
|
12462
|
+
exports.LlmKeyMissingError = exports.SELF_HOSTED_USER_RULE_OVERRIDE = exports.DECISION_EXTRACTION_SYSTEM_PROMPT = void 0;
|
|
12438
12463
|
exports.stripMarkdownJsonFence = stripMarkdownJsonFence2;
|
|
12439
12464
|
exports.parseExtractedDecision = parseExtractedDecision;
|
|
12440
12465
|
exports.extractDecisionLlm = extractDecisionLlm;
|
|
@@ -12464,6 +12489,9 @@ Fields:
|
|
|
12464
12489
|
Output ONLY valid JSON. If no decision: {"decision": null, "rationale": null, "rejected": [], "confidence": 0}.
|
|
12465
12490
|
Never add explanation outside the JSON.
|
|
12466
12491
|
Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"option": string, "reason": string}], "confidence": number}`;
|
|
12492
|
+
exports.SELF_HOSTED_USER_RULE_OVERRIDE = `User-stated rule wins:
|
|
12493
|
+
- A durable rule is a standing instruction for future work in this repo or team (e.g. "from now on", "always", "never", "we no longer"). An instruction scoped to the current task, file, or step (e.g. "don't use that file", "stop logging here", "don't add tests for this one") is not durable; apply the rules above to it.
|
|
12494
|
+
- If the user states a durable rule in this turn, extract that rule even when the assistant refuses it, cites an older rule, or says it will not follow the new one. In that case do not return null, and do not extract the older rule the assistant restates.`;
|
|
12467
12495
|
var LlmKeyMissingError = class extends Error {
|
|
12468
12496
|
constructor(message) {
|
|
12469
12497
|
super(message);
|
|
@@ -12471,10 +12499,9 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
|
|
|
12471
12499
|
}
|
|
12472
12500
|
};
|
|
12473
12501
|
exports.LlmKeyMissingError = LlmKeyMissingError;
|
|
12502
|
+
var EXTRACTION_MAX_TOKENS = 600;
|
|
12474
12503
|
function stripMarkdownJsonFence2(raw) {
|
|
12475
|
-
|
|
12476
|
-
const m2 = t2.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
12477
|
-
return m2?.[1]?.trim() ?? t2;
|
|
12504
|
+
return raw.trim().replace(/^```[a-z]*\s*/i, "").replace(/\s*```\s*$/, "").trim();
|
|
12478
12505
|
}
|
|
12479
12506
|
function parseExtractedDecision(rawText) {
|
|
12480
12507
|
const raw = JSON.parse(stripMarkdownJsonFence2(rawText));
|
|
@@ -12491,6 +12518,9 @@ Schema: {"decision": string|null, "rationale": string|null, "rejected": [{"optio
|
|
|
12491
12518
|
const userPrompt = `Session turn:
|
|
12492
12519
|
User: ${userMessage}
|
|
12493
12520
|
Claude: ${claudeReply}`;
|
|
12521
|
+
const systemPrompt = cfg.extraSystemRules ? `${exports.DECISION_EXTRACTION_SYSTEM_PROMPT}
|
|
12522
|
+
|
|
12523
|
+
${cfg.extraSystemRules}` : exports.DECISION_EXTRACTION_SYSTEM_PROMPT;
|
|
12494
12524
|
let rawText;
|
|
12495
12525
|
if (cfg.provider === "openai") {
|
|
12496
12526
|
const baseUrl = cfg.openaiBaseUrl ?? (0, llm_js_1.resolveOpenAiBaseUrl)();
|
|
@@ -12500,9 +12530,9 @@ Claude: ${claudeReply}`;
|
|
|
12500
12530
|
rawText = await (0, llm_js_1.openaiChat)({
|
|
12501
12531
|
apiKey: cfg.openaiApiKey ?? "",
|
|
12502
12532
|
model: cfg.openaiModel,
|
|
12503
|
-
system:
|
|
12533
|
+
system: systemPrompt,
|
|
12504
12534
|
user: userPrompt,
|
|
12505
|
-
maxTokens:
|
|
12535
|
+
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
12506
12536
|
baseUrl,
|
|
12507
12537
|
json: true
|
|
12508
12538
|
});
|
|
@@ -12513,9 +12543,13 @@ Claude: ${claudeReply}`;
|
|
|
12513
12543
|
rawText = await (0, llm_js_1.anthropicChat)({
|
|
12514
12544
|
apiKey: cfg.anthropicApiKey.trim(),
|
|
12515
12545
|
model: cfg.anthropicModel,
|
|
12516
|
-
system:
|
|
12546
|
+
system: systemPrompt,
|
|
12517
12547
|
user: userPrompt,
|
|
12518
|
-
maxTokens:
|
|
12548
|
+
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
12549
|
+
// Same turn, same extraction: the Anthropic API samples at temperature 1
|
|
12550
|
+
// unless told otherwise, which made a clear decision come back as
|
|
12551
|
+
// "no decision" on some runs and get extracted on others.
|
|
12552
|
+
temperature: 0
|
|
12519
12553
|
});
|
|
12520
12554
|
}
|
|
12521
12555
|
return parseExtractedDecision(rawText);
|