blun-king-cli 9.1.396 → 9.1.398
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/LIESMICH.txt +25 -0
- package/README.md +23 -0
- package/bin/goal-completion-evidence-policy.cjs +57 -0
- package/bin/telegram-private-conversation-policy.cjs +141 -0
- package/blun.mjs +24 -2
- package/package.json +1 -1
- package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +141 -0
- package/telegram-plugin/dist/bridge.mjs +17 -1
- package/telegram-plugin/dist/mcp-server.mjs +10 -27
package/LIESMICH.txt
CHANGED
|
@@ -381,6 +381,31 @@ Anweisung wie "Zeig mir den letzten Telegram-Verlauf" das passende Werkzeug im
|
|
|
381
381
|
selben Modellschritt erhalten, während Begrüßungen und fachfremde Anfragen keine
|
|
382
382
|
zusätzlichen Schemata laden.
|
|
383
383
|
|
|
384
|
+
Abschlussbedingungen mit Laufzeitbeleg
|
|
385
|
+
--------------------------------------
|
|
386
|
+
|
|
387
|
+
Ab BLUN King 9.1.397 kann ein autonomes Ziel mit ausdrücklich gesetzter
|
|
388
|
+
Abschlussbedingung nicht mehr allein aufgrund einer Textbehauptung als fertig
|
|
389
|
+
markiert werden. Vor dem Abschluss muss ein aktueller Prüf-Checkpoint vorliegen,
|
|
390
|
+
der auf mindestens einem erfolgreichen Laufzeitwerkzeug beruht und als verifiziert
|
|
391
|
+
eingestuft ist. Fehlt dieser Beleg, bleibt das Ziel aktiv und King erhält eine
|
|
392
|
+
konkrete Nachbesserung statt einer falschen Fertigmeldung. Ziele ohne ausdrücklich
|
|
393
|
+
gesetzte Abschlussbedingung behalten ihr bisheriges Verhalten.
|
|
394
|
+
|
|
395
|
+
Keine doppelte Telegram-Antwort ohne neue Nachricht
|
|
396
|
+
---------------------------------------------------
|
|
397
|
+
|
|
398
|
+
Ab BLUN King 9.1.398 prüft jeder Text-Ausgangspfad vor dem Senden die jüngste
|
|
399
|
+
erfolgreiche Telegram-Antwort und den jüngsten Eingang desselben Chats. Solange
|
|
400
|
+
danach keine neue Nutzernachricht eingetroffen ist, wird eine wortgleiche oder
|
|
401
|
+
nahezu gleiche Wiederholung nicht erneut gesendet. Das gilt gleichermaßen für
|
|
402
|
+
den Telegram-Werkzeugpfad und beide automatischen Rückfallpfade.
|
|
403
|
+
|
|
404
|
+
Nach einer neuen Nutzernachricht ist dieselbe Antwort wieder zulässig. Andere
|
|
405
|
+
Ergebnisse und Datei-Anhänge bleiben unverändert. Die Prüfung liest nur begrenzte
|
|
406
|
+
Endbereiche der Ein- und Ausgangsprotokolle und fällt bei fehlendem Beleg offen
|
|
407
|
+
zurück, damit Telegram nicht wegen einer beschädigten Protokollzeile blockiert.
|
|
408
|
+
|
|
384
409
|
Angehängte Bilder werden weiterhin mit ReadMediaFile gelesen. Bild-, Video- und
|
|
385
410
|
Spracherzeugung laufen asynchron, sodass die Konsole während der Verarbeitung
|
|
386
411
|
nutzbar bleibt. GenerateVideo kann außerdem einen abgeschlossenen Bildauftrag
|
package/README.md
CHANGED
|
@@ -391,6 +391,29 @@ Anweisung wie „Zeig mir den letzten Telegram-Verlauf“ das passende Werkzeug
|
|
|
391
391
|
selben Modellschritt erhalten, während Begrüßungen und fachfremde Anfragen keine
|
|
392
392
|
zusätzlichen Schemata laden.
|
|
393
393
|
|
|
394
|
+
## Abschlussbedingungen mit Laufzeitbeleg
|
|
395
|
+
|
|
396
|
+
Ab BLUN King 9.1.397 kann ein autonomes Ziel mit ausdrücklich gesetzter
|
|
397
|
+
Abschlussbedingung nicht mehr allein aufgrund einer Textbehauptung als fertig
|
|
398
|
+
markiert werden. Vor dem Abschluss muss ein aktueller Prüf-Checkpoint vorliegen,
|
|
399
|
+
der auf mindestens einem erfolgreichen Laufzeitwerkzeug beruht und als verifiziert
|
|
400
|
+
eingestuft ist. Fehlt dieser Beleg, bleibt das Ziel aktiv und King erhält eine
|
|
401
|
+
konkrete Nachbesserung statt einer falschen Fertigmeldung. Ziele ohne ausdrücklich
|
|
402
|
+
gesetzte Abschlussbedingung behalten ihr bisheriges Verhalten.
|
|
403
|
+
|
|
404
|
+
## Keine doppelte Telegram-Antwort ohne neue Nachricht
|
|
405
|
+
|
|
406
|
+
Ab BLUN King 9.1.398 prüft jeder Text-Ausgangspfad vor dem Senden die jüngste
|
|
407
|
+
erfolgreiche Telegram-Antwort und den jüngsten Eingang desselben Chats. Solange
|
|
408
|
+
danach keine neue Nutzernachricht eingetroffen ist, wird eine wortgleiche oder
|
|
409
|
+
nahezu gleiche Wiederholung nicht erneut gesendet. Das gilt gleichermaßen für
|
|
410
|
+
den Telegram-Werkzeugpfad und beide automatischen Rückfallpfade.
|
|
411
|
+
|
|
412
|
+
Nach einer neuen Nutzernachricht ist dieselbe Antwort wieder zulässig. Andere
|
|
413
|
+
Ergebnisse und Datei-Anhänge bleiben unverändert. Die Prüfung liest nur begrenzte
|
|
414
|
+
Endbereiche der Ein- und Ausgangsprotokolle und fällt bei fehlendem Beleg offen
|
|
415
|
+
zurück, damit Telegram nicht wegen einer beschädigten Protokollzeile blockiert.
|
|
416
|
+
|
|
394
417
|
Angehängte Bilder werden weiterhin mit `ReadMediaFile` gelesen. Bild-, Video-
|
|
395
418
|
und Spracherzeugung laufen asynchron, sodass die Konsole während der Verarbeitung
|
|
396
419
|
nutzbar bleibt. `GenerateVideo` kann außerdem einen abgeschlossenen Bildauftrag
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function hasCompletionCriterion(goal) {
|
|
4
|
+
return typeof goal?.completionCriterion === 'string'
|
|
5
|
+
&& goal.completionCriterion.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function successfulRuntimeEvidence(checkpoint) {
|
|
9
|
+
const successfulTools = Number(checkpoint?.evidenceReceipt?.successfulTools);
|
|
10
|
+
return Number.isSafeInteger(successfulTools) && successfulTools > 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function evaluateGoalCompletionEvidence(goal) {
|
|
14
|
+
if (!hasCompletionCriterion(goal)) {
|
|
15
|
+
return {
|
|
16
|
+
required: false,
|
|
17
|
+
allPassed: true,
|
|
18
|
+
result: 'satisfied',
|
|
19
|
+
gaps: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const checkpoint = goal?.actionCheckpoint;
|
|
24
|
+
if (!checkpoint || typeof checkpoint !== 'object' || Array.isArray(checkpoint)) {
|
|
25
|
+
return {
|
|
26
|
+
required: true,
|
|
27
|
+
allPassed: false,
|
|
28
|
+
result: 'needs_revision',
|
|
29
|
+
gaps: ['Save a verify-phase action checkpoint before completing the goal.'],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const gaps = [];
|
|
34
|
+
if (checkpoint.phase !== 'verify') {
|
|
35
|
+
gaps.push('The latest action checkpoint must be in the verify phase.');
|
|
36
|
+
}
|
|
37
|
+
if (checkpoint.evidenceBasis !== 'runtime_tool') {
|
|
38
|
+
gaps.push('The completion proof must come from a runtime tool.');
|
|
39
|
+
}
|
|
40
|
+
if (checkpoint.epistemicState !== 'verified') {
|
|
41
|
+
gaps.push('The completion proof must be classified as verified.');
|
|
42
|
+
}
|
|
43
|
+
if (!successfulRuntimeEvidence(checkpoint)) {
|
|
44
|
+
gaps.push('The runtime evidence receipt must contain at least one successful tool result.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
required: true,
|
|
49
|
+
allPassed: gaps.length === 0,
|
|
50
|
+
result: gaps.length === 0 ? 'satisfied' : 'needs_revision',
|
|
51
|
+
gaps,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
evaluateGoalCompletionEvidence,
|
|
57
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { closeSync, openSync, readSync, statSync } = require('node:fs');
|
|
4
|
+
|
|
3
5
|
const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
|
|
4
6
|
const WORK_PERMISSION_QUESTION = /^(?:kann|darf|soll) ich\b.{0,100}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b/u;
|
|
5
7
|
const TRAILING_WORK_PERMISSION_QUESTION = /(?:^|\r?\n\s*\r?\n)(?:kann|darf|soll)\s+ich\b[^\r\n]{0,160}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b[^\r\n]*$/iu;
|
|
@@ -10,6 +12,12 @@ const MEDIA_PROGRESS_MARKER = /\b(?:bildgenerierung|bildjob|generateimage|getmed
|
|
|
10
12
|
const MEDIA_PENDING_STATE = /\b(?:auftrag wurde angenommen|job wurde angenommen|job angenommen|aufruf wurde abgesetzt|angestossen|angestoßen|processing|verarbeitung)\b/u;
|
|
11
13
|
const MEDIA_NO_RESULT = /\b(?:kein neues bild|noch kein bild|kein bild liefern|nichts angekommen|nichts neues gelandet|nicht verfuegbar|nicht verfügbar|getmedia fehlt)\b/u;
|
|
12
14
|
|
|
15
|
+
const DUPLICATE_LOG_TAIL_BYTES = 512 * 1024;
|
|
16
|
+
const DUPLICATE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const NEAR_DUPLICATE_MIN_CHARS = 120;
|
|
18
|
+
const NEAR_DUPLICATE_SIMILARITY = 0.88;
|
|
19
|
+
const SUCCESSFUL_REPLY_KINDS = new Set(['reply', 'reply-fallback']);
|
|
20
|
+
|
|
13
21
|
function normalize(text) {
|
|
14
22
|
return String(text ?? '')
|
|
15
23
|
.normalize('NFKC')
|
|
@@ -19,6 +27,133 @@ function normalize(text) {
|
|
|
19
27
|
.trim();
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
function readJsonlTail(file, maxBytes = DUPLICATE_LOG_TAIL_BYTES) {
|
|
31
|
+
try {
|
|
32
|
+
const size = statSync(file).size;
|
|
33
|
+
if (size <= 0) return [];
|
|
34
|
+
const start = Math.max(0, size - maxBytes);
|
|
35
|
+
const length = size - start;
|
|
36
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
37
|
+
const descriptor = openSync(file, 'r');
|
|
38
|
+
let bytesRead = 0;
|
|
39
|
+
try {
|
|
40
|
+
while (bytesRead < length) {
|
|
41
|
+
const count = readSync(descriptor, buffer, bytesRead, length - bytesRead, start + bytesRead);
|
|
42
|
+
if (count === 0) break;
|
|
43
|
+
bytesRead += count;
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
closeSync(descriptor);
|
|
47
|
+
}
|
|
48
|
+
let raw = buffer.subarray(0, bytesRead).toString('utf8');
|
|
49
|
+
if (start > 0) {
|
|
50
|
+
const firstNewline = raw.indexOf('\n');
|
|
51
|
+
raw = firstNewline < 0 ? '' : raw.slice(firstNewline + 1);
|
|
52
|
+
}
|
|
53
|
+
return raw.split(/\r?\n/u).flatMap((line) => {
|
|
54
|
+
if (line.trim().length === 0) return [];
|
|
55
|
+
try {
|
|
56
|
+
const entry = JSON.parse(line);
|
|
57
|
+
return typeof entry === 'object' && entry !== null ? [entry] : [];
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizedTokens(text) {
|
|
68
|
+
return normalize(text).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tokenBigrams(tokens) {
|
|
72
|
+
const bigrams = [];
|
|
73
|
+
for (let index = 0; index + 1 < tokens.length; index += 1) {
|
|
74
|
+
bigrams.push(`${tokens[index]}\u0000${tokens[index + 1]}`);
|
|
75
|
+
}
|
|
76
|
+
return bigrams;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diceSimilarity(leftItems, rightItems) {
|
|
80
|
+
const left = new Set(leftItems);
|
|
81
|
+
const right = new Set(rightItems);
|
|
82
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
83
|
+
let intersection = 0;
|
|
84
|
+
for (const item of left) {
|
|
85
|
+
if (right.has(item)) intersection += 1;
|
|
86
|
+
}
|
|
87
|
+
return (2 * intersection) / (left.size + right.size);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function outboundReplySimilarity(left, right) {
|
|
91
|
+
const leftNormalized = normalize(left);
|
|
92
|
+
const rightNormalized = normalize(right);
|
|
93
|
+
if (leftNormalized.length === 0 || rightNormalized.length === 0) return 0;
|
|
94
|
+
if (leftNormalized === rightNormalized) return 1;
|
|
95
|
+
if (leftNormalized.length < NEAR_DUPLICATE_MIN_CHARS || rightNormalized.length < NEAR_DUPLICATE_MIN_CHARS) return 0;
|
|
96
|
+
const leftTokens = normalizedTokens(leftNormalized);
|
|
97
|
+
const rightTokens = normalizedTokens(rightNormalized);
|
|
98
|
+
if (leftTokens.length < 12 || rightTokens.length < 12) return 0;
|
|
99
|
+
const tokenScore = diceSimilarity(leftTokens, rightTokens);
|
|
100
|
+
const bigramScore = diceSimilarity(tokenBigrams(leftTokens), tokenBigrams(rightTokens));
|
|
101
|
+
return (tokenScore + bigramScore) / 2;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function entryTime(entry) {
|
|
105
|
+
const raw = entry?.ts ?? entry?.meta?.timestamp;
|
|
106
|
+
const value = Date.parse(String(raw ?? ''));
|
|
107
|
+
return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function entryChatId(entry) {
|
|
111
|
+
return String(entry?.chat_id ?? entry?.meta?.chat_id ?? '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findDuplicateReplyWithoutNewInbound({
|
|
115
|
+
chatId,
|
|
116
|
+
inboundFile,
|
|
117
|
+
now = Date.now(),
|
|
118
|
+
outboxFile,
|
|
119
|
+
text,
|
|
120
|
+
}) {
|
|
121
|
+
const wantedChat = String(chatId ?? '').trim();
|
|
122
|
+
const candidate = String(text ?? '').trim();
|
|
123
|
+
if (wantedChat.length === 0 || candidate.length === 0) return null;
|
|
124
|
+
|
|
125
|
+
const outbound = readJsonlTail(outboxFile).filter((entry) => (
|
|
126
|
+
SUCCESSFUL_REPLY_KINDS.has(entry.kind)
|
|
127
|
+
&& entryChatId(entry) === wantedChat
|
|
128
|
+
&& typeof entry.text === 'string'
|
|
129
|
+
));
|
|
130
|
+
if (outbound.length === 0) return null;
|
|
131
|
+
const latestOutboundAt = Math.max(...outbound.map(entryTime));
|
|
132
|
+
if (!Number.isFinite(latestOutboundAt)) return null;
|
|
133
|
+
|
|
134
|
+
const inboundTimes = readJsonlTail(inboundFile)
|
|
135
|
+
.filter((entry) => entryChatId(entry) === wantedChat)
|
|
136
|
+
.map(entryTime)
|
|
137
|
+
.filter(Number.isFinite);
|
|
138
|
+
if (inboundTimes.length === 0) return null;
|
|
139
|
+
if (Math.max(...inboundTimes) > latestOutboundAt) return null;
|
|
140
|
+
|
|
141
|
+
const cutoff = Number(now) - DUPLICATE_MAX_AGE_MS;
|
|
142
|
+
for (let index = outbound.length - 1; index >= 0; index -= 1) {
|
|
143
|
+
const previous = outbound[index];
|
|
144
|
+
const previousAt = entryTime(previous);
|
|
145
|
+
if (previousAt < cutoff || previousAt > Number(now)) continue;
|
|
146
|
+
const similarity = outboundReplySimilarity(previous.text, candidate);
|
|
147
|
+
if (similarity < NEAR_DUPLICATE_SIMILARITY) continue;
|
|
148
|
+
return {
|
|
149
|
+
kind: previous.kind,
|
|
150
|
+
similarity,
|
|
151
|
+
ts: previous.ts,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
22
157
|
function sanitizePrivateConversationReply(chatId, text) {
|
|
23
158
|
const value = String(text ?? '');
|
|
24
159
|
if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return value;
|
|
@@ -38,6 +173,12 @@ function isPrivateInternalStatusReply(chatId, text) {
|
|
|
38
173
|
}
|
|
39
174
|
|
|
40
175
|
module.exports = {
|
|
176
|
+
DUPLICATE_LOG_TAIL_BYTES,
|
|
177
|
+
DUPLICATE_MAX_AGE_MS,
|
|
178
|
+
NEAR_DUPLICATE_MIN_CHARS,
|
|
179
|
+
NEAR_DUPLICATE_SIMILARITY,
|
|
180
|
+
findDuplicateReplyWithoutNewInbound,
|
|
41
181
|
isPrivateInternalStatusReply,
|
|
182
|
+
outboundReplySimilarity,
|
|
42
183
|
sanitizePrivateConversationReply,
|
|
43
184
|
};
|
package/blun.mjs
CHANGED
|
@@ -21447,6 +21447,7 @@ var { projectSoulText } = createRequire(import.meta.url)("./bin/soul-organizatio
|
|
|
21447
21447
|
var { soulFileInstruction } = createRequire(import.meta.url)("./bin/soul-preservation-policy.cjs");
|
|
21448
21448
|
var { readProfilePersona, resolveSoulFile } = createRequire(import.meta.url)("./bin/profile-identity-resolution.cjs");
|
|
21449
21449
|
var { advanceActionEvidenceReceipt, assertActionCheckpointEvidenceBasis, assertActionCheckpointRevision, emptyActionEvidenceReceipt, normalizeActionCheckpoint, projectActionCheckpoint } = createRequire(import.meta.url)("./bin/cognitive-action-checkpoint.cjs");
|
|
21450
|
+
var { evaluateGoalCompletionEvidence } = createRequire(import.meta.url)("./bin/goal-completion-evidence-policy.cjs");
|
|
21450
21451
|
async function prepareSystemPromptContext(kaos, brandHome, options) {
|
|
21451
21452
|
const additionalDirs = normalizeAdditionalDirs(options?.additionalDirs ?? []);
|
|
21452
21453
|
const [cwdListing, agentsMdResult, additionalDirsInfo] = await Promise.all([
|
|
@@ -230394,6 +230395,10 @@ var init_goal$1 = __esmMin((() => {
|
|
|
230394
230395
|
async markComplete(input = {}, actor = "model") {
|
|
230395
230396
|
const state = this.state;
|
|
230396
230397
|
if (state === void 0 || state.status !== "active") return null;
|
|
230398
|
+
const completionEvidence = evaluateGoalCompletionEvidence(state);
|
|
230399
|
+
if (!completionEvidence.allPassed) {
|
|
230400
|
+
throw new BlunError(ErrorCodes.GOAL_STATUS_INVALID, `Completion criterion needs revision: ${completionEvidence.gaps.join(" ")} Run the required verification, save a runtime-backed verify checkpoint, then mark the goal complete.`);
|
|
230401
|
+
}
|
|
230397
230402
|
const mcInjector = this.agent.injection?.getMissionContractInjector();
|
|
230398
230403
|
if (mcInjector) {
|
|
230399
230404
|
const evaluation = mcInjector.evaluate();
|
|
@@ -262710,7 +262715,7 @@ var init_outcome_prompts = __esmMin((() => {}));
|
|
|
262710
262715
|
//#region ../../packages/agent-core/src/tools/builtin/goal/update-goal.md?raw
|
|
262711
262716
|
var update_goal_default;
|
|
262712
262717
|
var init_update_goal$1 = __esmMin((() => {
|
|
262713
|
-
update_goal_default = "Update the current autonomous goal. Set `status` only for a lifecycle change. After a coherent work slice, save `actionCheckpoint` with a monotone revision, the last verified result, exact next action, expected evidence, and an explicit evidence basis. Use `runtime_tool` only when a successful tool in this turn measured the result; use `user_statement` for a direct user assertion, `external_report` for a report not independently measured here, and `carried_forward` only when the last verified text is unchanged. Classify knowledge as `verified`, `credible_unverified`, `hypothesis`, `uncertain_memory`, `stale`, or `unknown`; never present a weaker state as verified, and preserve the state on carry-forward. Start at revision 1 and increment the currently projected revision by exactly one; stale writers fail closed. This is durable progress state, not permission, and should change only when the facts change. A checkpoint-only call keeps the goal active.\n\n- `active` — resume a paused or blocked goal when the user explicitly asks you to work on that goal.\n- `complete` — the objective is fully satisfied, all files are written, all tests pass, and any stated validation has passed.\n- `blocked` — a genuine external condition or required user decision prevents progress.\n- `paused` — set the goal aside for now.\n\nDo not mark complete after a plan or partial result. If useful work remains, checkpoint it and continue. Do not ask for permission merely to execute an already authorized checkpoint; ask only at a real rights boundary or missing user decision.\n";
|
|
262718
|
+
update_goal_default = "Update the current autonomous goal. Set `status` only for a lifecycle change. After a coherent work slice, save `actionCheckpoint` with a monotone revision, the last verified result, exact next action, expected evidence, and an explicit evidence basis. Use `runtime_tool` only when a successful tool in this turn measured the result; use `user_statement` for a direct user assertion, `external_report` for a report not independently measured here, and `carried_forward` only when the last verified text is unchanged. Classify knowledge as `verified`, `credible_unverified`, `hypothesis`, `uncertain_memory`, `stale`, or `unknown`; never present a weaker state as verified, and preserve the state on carry-forward. Start at revision 1 and increment the currently projected revision by exactly one; stale writers fail closed. This is durable progress state, not permission, and should change only when the facts change. A checkpoint-only call keeps the goal active.\n\n- `active` — resume a paused or blocked goal when the user explicitly asks you to work on that goal.\n- `complete` — the objective is fully satisfied, all files are written, all tests pass, and any stated validation has passed. When the goal has a completion criterion, first save a `verify` checkpoint with `runtime_tool`, `verified`, and a successful runtime evidence receipt.\n- `blocked` — a genuine external condition or required user decision prevents progress.\n- `paused` — set the goal aside for now.\n\nDo not mark complete after a plan or partial result. If useful work remains, checkpoint it and continue. Do not ask for permission merely to execute an already authorized checkpoint; ask only at a real rights boundary or missing user decision.\n";
|
|
262714
262719
|
}));
|
|
262715
262720
|
//#endregion
|
|
262716
262721
|
//#region ../../packages/agent-core/src/tools/builtin/goal/update-goal.ts
|
|
@@ -514724,7 +514729,7 @@ var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
|
|
|
514724
514729
|
var createMediaAutoRetrievalController, parseAcceptedMediaJob, validateCompletedMedia;
|
|
514725
514730
|
({ createMediaAutoRetrievalController, parseAcceptedMediaJob } = createRequire(import.meta.url)("./bin/media-auto-retrieval-policy.cjs"));
|
|
514726
514731
|
({ validateCompletedMedia } = createRequire(import.meta.url)("./bin/media-result-policy.cjs"));
|
|
514727
|
-
var { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
|
|
514732
|
+
var { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
|
|
514728
514733
|
const TELEGRAM_TEXT_LIMIT = 4096;
|
|
514729
514734
|
const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
|
|
514730
514735
|
function mediaTelegramTarget(filePath) {
|
|
@@ -514833,6 +514838,23 @@ async function sendReplyFallback(chatId, text, contextOnly = false) {
|
|
|
514833
514838
|
} catch {}
|
|
514834
514839
|
return true;
|
|
514835
514840
|
}
|
|
514841
|
+
if (findDuplicateReplyWithoutNewInbound({
|
|
514842
|
+
chatId,
|
|
514843
|
+
inboundFile: join$4(channelDir(), "inbox.jsonl"),
|
|
514844
|
+
outboxFile: outboxPath(),
|
|
514845
|
+
text
|
|
514846
|
+
}) !== null) {
|
|
514847
|
+
try {
|
|
514848
|
+
appendFileSync(outboxPath(), `${JSON.stringify({
|
|
514849
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
514850
|
+
direction: "out",
|
|
514851
|
+
kind: "reply-fallback-suppressed-duplicate-without-new-inbound",
|
|
514852
|
+
chat_id: String(chatId),
|
|
514853
|
+
text
|
|
514854
|
+
})}\n`);
|
|
514855
|
+
} catch {}
|
|
514856
|
+
return true;
|
|
514857
|
+
}
|
|
514836
514858
|
const privateSafeText = sanitizePrivateConversationReply(chatId, text);
|
|
514837
514859
|
if (isGroupChat(chatId) && isGroupSuppressed(text, contextOnly)) {
|
|
514838
514860
|
try {
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { closeSync, openSync, readSync, statSync } = require('node:fs');
|
|
4
|
+
|
|
3
5
|
const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
|
|
4
6
|
const WORK_PERMISSION_QUESTION = /^(?:kann|darf|soll) ich\b.{0,100}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b/u;
|
|
5
7
|
const TRAILING_WORK_PERMISSION_QUESTION = /(?:^|\r?\n\s*\r?\n)(?:kann|darf|soll)\s+ich\b[^\r\n]{0,160}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b[^\r\n]*$/iu;
|
|
@@ -10,6 +12,12 @@ const MEDIA_PROGRESS_MARKER = /\b(?:bildgenerierung|bildjob|generateimage|getmed
|
|
|
10
12
|
const MEDIA_PENDING_STATE = /\b(?:auftrag wurde angenommen|job wurde angenommen|job angenommen|aufruf wurde abgesetzt|angestossen|angestoßen|processing|verarbeitung)\b/u;
|
|
11
13
|
const MEDIA_NO_RESULT = /\b(?:kein neues bild|noch kein bild|kein bild liefern|nichts angekommen|nichts neues gelandet|nicht verfuegbar|nicht verfügbar|getmedia fehlt)\b/u;
|
|
12
14
|
|
|
15
|
+
const DUPLICATE_LOG_TAIL_BYTES = 512 * 1024;
|
|
16
|
+
const DUPLICATE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const NEAR_DUPLICATE_MIN_CHARS = 120;
|
|
18
|
+
const NEAR_DUPLICATE_SIMILARITY = 0.88;
|
|
19
|
+
const SUCCESSFUL_REPLY_KINDS = new Set(['reply', 'reply-fallback']);
|
|
20
|
+
|
|
13
21
|
function normalize(text) {
|
|
14
22
|
return String(text ?? '')
|
|
15
23
|
.normalize('NFKC')
|
|
@@ -19,6 +27,133 @@ function normalize(text) {
|
|
|
19
27
|
.trim();
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
function readJsonlTail(file, maxBytes = DUPLICATE_LOG_TAIL_BYTES) {
|
|
31
|
+
try {
|
|
32
|
+
const size = statSync(file).size;
|
|
33
|
+
if (size <= 0) return [];
|
|
34
|
+
const start = Math.max(0, size - maxBytes);
|
|
35
|
+
const length = size - start;
|
|
36
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
37
|
+
const descriptor = openSync(file, 'r');
|
|
38
|
+
let bytesRead = 0;
|
|
39
|
+
try {
|
|
40
|
+
while (bytesRead < length) {
|
|
41
|
+
const count = readSync(descriptor, buffer, bytesRead, length - bytesRead, start + bytesRead);
|
|
42
|
+
if (count === 0) break;
|
|
43
|
+
bytesRead += count;
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
closeSync(descriptor);
|
|
47
|
+
}
|
|
48
|
+
let raw = buffer.subarray(0, bytesRead).toString('utf8');
|
|
49
|
+
if (start > 0) {
|
|
50
|
+
const firstNewline = raw.indexOf('\n');
|
|
51
|
+
raw = firstNewline < 0 ? '' : raw.slice(firstNewline + 1);
|
|
52
|
+
}
|
|
53
|
+
return raw.split(/\r?\n/u).flatMap((line) => {
|
|
54
|
+
if (line.trim().length === 0) return [];
|
|
55
|
+
try {
|
|
56
|
+
const entry = JSON.parse(line);
|
|
57
|
+
return typeof entry === 'object' && entry !== null ? [entry] : [];
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizedTokens(text) {
|
|
68
|
+
return normalize(text).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tokenBigrams(tokens) {
|
|
72
|
+
const bigrams = [];
|
|
73
|
+
for (let index = 0; index + 1 < tokens.length; index += 1) {
|
|
74
|
+
bigrams.push(`${tokens[index]}\u0000${tokens[index + 1]}`);
|
|
75
|
+
}
|
|
76
|
+
return bigrams;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diceSimilarity(leftItems, rightItems) {
|
|
80
|
+
const left = new Set(leftItems);
|
|
81
|
+
const right = new Set(rightItems);
|
|
82
|
+
if (left.size === 0 || right.size === 0) return 0;
|
|
83
|
+
let intersection = 0;
|
|
84
|
+
for (const item of left) {
|
|
85
|
+
if (right.has(item)) intersection += 1;
|
|
86
|
+
}
|
|
87
|
+
return (2 * intersection) / (left.size + right.size);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function outboundReplySimilarity(left, right) {
|
|
91
|
+
const leftNormalized = normalize(left);
|
|
92
|
+
const rightNormalized = normalize(right);
|
|
93
|
+
if (leftNormalized.length === 0 || rightNormalized.length === 0) return 0;
|
|
94
|
+
if (leftNormalized === rightNormalized) return 1;
|
|
95
|
+
if (leftNormalized.length < NEAR_DUPLICATE_MIN_CHARS || rightNormalized.length < NEAR_DUPLICATE_MIN_CHARS) return 0;
|
|
96
|
+
const leftTokens = normalizedTokens(leftNormalized);
|
|
97
|
+
const rightTokens = normalizedTokens(rightNormalized);
|
|
98
|
+
if (leftTokens.length < 12 || rightTokens.length < 12) return 0;
|
|
99
|
+
const tokenScore = diceSimilarity(leftTokens, rightTokens);
|
|
100
|
+
const bigramScore = diceSimilarity(tokenBigrams(leftTokens), tokenBigrams(rightTokens));
|
|
101
|
+
return (tokenScore + bigramScore) / 2;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function entryTime(entry) {
|
|
105
|
+
const raw = entry?.ts ?? entry?.meta?.timestamp;
|
|
106
|
+
const value = Date.parse(String(raw ?? ''));
|
|
107
|
+
return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function entryChatId(entry) {
|
|
111
|
+
return String(entry?.chat_id ?? entry?.meta?.chat_id ?? '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findDuplicateReplyWithoutNewInbound({
|
|
115
|
+
chatId,
|
|
116
|
+
inboundFile,
|
|
117
|
+
now = Date.now(),
|
|
118
|
+
outboxFile,
|
|
119
|
+
text,
|
|
120
|
+
}) {
|
|
121
|
+
const wantedChat = String(chatId ?? '').trim();
|
|
122
|
+
const candidate = String(text ?? '').trim();
|
|
123
|
+
if (wantedChat.length === 0 || candidate.length === 0) return null;
|
|
124
|
+
|
|
125
|
+
const outbound = readJsonlTail(outboxFile).filter((entry) => (
|
|
126
|
+
SUCCESSFUL_REPLY_KINDS.has(entry.kind)
|
|
127
|
+
&& entryChatId(entry) === wantedChat
|
|
128
|
+
&& typeof entry.text === 'string'
|
|
129
|
+
));
|
|
130
|
+
if (outbound.length === 0) return null;
|
|
131
|
+
const latestOutboundAt = Math.max(...outbound.map(entryTime));
|
|
132
|
+
if (!Number.isFinite(latestOutboundAt)) return null;
|
|
133
|
+
|
|
134
|
+
const inboundTimes = readJsonlTail(inboundFile)
|
|
135
|
+
.filter((entry) => entryChatId(entry) === wantedChat)
|
|
136
|
+
.map(entryTime)
|
|
137
|
+
.filter(Number.isFinite);
|
|
138
|
+
if (inboundTimes.length === 0) return null;
|
|
139
|
+
if (Math.max(...inboundTimes) > latestOutboundAt) return null;
|
|
140
|
+
|
|
141
|
+
const cutoff = Number(now) - DUPLICATE_MAX_AGE_MS;
|
|
142
|
+
for (let index = outbound.length - 1; index >= 0; index -= 1) {
|
|
143
|
+
const previous = outbound[index];
|
|
144
|
+
const previousAt = entryTime(previous);
|
|
145
|
+
if (previousAt < cutoff || previousAt > Number(now)) continue;
|
|
146
|
+
const similarity = outboundReplySimilarity(previous.text, candidate);
|
|
147
|
+
if (similarity < NEAR_DUPLICATE_SIMILARITY) continue;
|
|
148
|
+
return {
|
|
149
|
+
kind: previous.kind,
|
|
150
|
+
similarity,
|
|
151
|
+
ts: previous.ts,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
22
157
|
function sanitizePrivateConversationReply(chatId, text) {
|
|
23
158
|
const value = String(text ?? '');
|
|
24
159
|
if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return value;
|
|
@@ -38,6 +173,12 @@ function isPrivateInternalStatusReply(chatId, text) {
|
|
|
38
173
|
}
|
|
39
174
|
|
|
40
175
|
module.exports = {
|
|
176
|
+
DUPLICATE_LOG_TAIL_BYTES,
|
|
177
|
+
DUPLICATE_MAX_AGE_MS,
|
|
178
|
+
NEAR_DUPLICATE_MIN_CHARS,
|
|
179
|
+
NEAR_DUPLICATE_SIMILARITY,
|
|
180
|
+
findDuplicateReplyWithoutNewInbound,
|
|
41
181
|
isPrivateInternalStatusReply,
|
|
182
|
+
outboundReplySimilarity,
|
|
42
183
|
sanitizePrivateConversationReply,
|
|
43
184
|
};
|
|
@@ -11,7 +11,7 @@ import telegramApprovalRelay from "../../bin/telegram-approval-relay.cjs";
|
|
|
11
11
|
import privateConversationPolicy from "../bin/telegram-private-conversation-policy.cjs";
|
|
12
12
|
const { buildTelegramRemoteStatus, resolveTelegramRemoteVersion } = remoteStatusPolicy;
|
|
13
13
|
const { buildTelegramApprovalCard, isAuthorizedTelegramApprovalCallback, listPendingTelegramApprovals, markTelegramApprovalSent, parseTelegramApprovalCallback, resolveTelegramApprovalTarget, wasTelegramApprovalSent, writeTelegramApprovalResponse } = telegramApprovalRelay;
|
|
14
|
-
const { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
|
|
14
|
+
const { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
|
|
15
15
|
//#region ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
|
|
16
16
|
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
17
17
|
function normalizeWindowsPath(input = "") {
|
|
@@ -4199,6 +4199,22 @@ async function sendReplyFallback(chatId, rawText) {
|
|
|
4199
4199
|
logLine(`reply-fallback suppressed (private internal): ${rawText.slice(0, 60)}`);
|
|
4200
4200
|
return true;
|
|
4201
4201
|
}
|
|
4202
|
+
if (findDuplicateReplyWithoutNewInbound({
|
|
4203
|
+
chatId,
|
|
4204
|
+
inboundFile: inboxLog(),
|
|
4205
|
+
outboxFile: outboxLog(),
|
|
4206
|
+
text: rawText
|
|
4207
|
+
}) !== null) {
|
|
4208
|
+
appendFileSync(outboxLog(), `${JSON.stringify({
|
|
4209
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4210
|
+
direction: "out",
|
|
4211
|
+
kind: "reply-fallback-suppressed-duplicate-without-new-inbound",
|
|
4212
|
+
chat_id: String(chatId),
|
|
4213
|
+
text: rawText
|
|
4214
|
+
})}\n`);
|
|
4215
|
+
logLine(`reply-fallback suppressed (duplicate without new inbound): ${rawText.slice(0, 60)}`);
|
|
4216
|
+
return true;
|
|
4217
|
+
}
|
|
4202
4218
|
const privateSafeText = sanitizePrivateConversationReply(chatId, rawText);
|
|
4203
4219
|
if (isGroupChat(chatId) && isGroupNoiseReply(rawText)) {
|
|
4204
4220
|
logLine(`reply-fallback suppressed (group meta-noise): ${rawText.slice(0, 60)}`);
|
|
@@ -9667,7 +9667,7 @@ var StdioServerTransport = class {
|
|
|
9667
9667
|
* chats the inbound gate would deliver from.
|
|
9668
9668
|
*/
|
|
9669
9669
|
var import_out = require_out();
|
|
9670
|
-
const { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
|
|
9670
|
+
const { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
|
|
9671
9671
|
const TOOL_DEFINITIONS = [
|
|
9672
9672
|
{
|
|
9673
9673
|
name: "reply",
|
|
@@ -9787,34 +9787,17 @@ async function runTool(api, token, name, args) {
|
|
|
9787
9787
|
}
|
|
9788
9788
|
}
|
|
9789
9789
|
/**
|
|
9790
|
-
* Idempotency guard
|
|
9791
|
-
*
|
|
9792
|
-
*
|
|
9793
|
-
* erneut gesendet — das Tool meldet dem Modell "bereits zugestellt", damit es
|
|
9794
|
-
* nicht weiter retryt. Datei-Anhänge sind nie betroffen.
|
|
9790
|
+
* Idempotency guard for text-only replies. If no newer inbound message exists,
|
|
9791
|
+
* an exact or near-identical successful reply is not sent again. Attachments
|
|
9792
|
+
* are never affected.
|
|
9795
9793
|
*/
|
|
9796
|
-
const DUPLICATE_WINDOW_MS = 3e5;
|
|
9797
|
-
const OUTBOX_TAIL_BYTES = 32768;
|
|
9798
9794
|
function isRecentDuplicate(chatId, text) {
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
const cutoff = Date.now() - DUPLICATE_WINDOW_MS;
|
|
9806
|
-
for (const line of tail.split("\n")) {
|
|
9807
|
-
if (line.trim().length === 0) continue;
|
|
9808
|
-
try {
|
|
9809
|
-
const entry = JSON.parse(line);
|
|
9810
|
-
if (entry.kind !== "reply" && entry.kind !== "reply-fallback") continue;
|
|
9811
|
-
if (String(entry.chat_id) !== String(chatId)) continue;
|
|
9812
|
-
if (entry.ts === void 0 || Date.parse(entry.ts) < cutoff) continue;
|
|
9813
|
-
if ((entry.text ?? "").trim() === wanted) return true;
|
|
9814
|
-
} catch {}
|
|
9815
|
-
}
|
|
9816
|
-
} catch {}
|
|
9817
|
-
return false;
|
|
9795
|
+
return findDuplicateReplyWithoutNewInbound({
|
|
9796
|
+
chatId,
|
|
9797
|
+
inboundFile: inboxLog(),
|
|
9798
|
+
outboxFile: outboxLog(),
|
|
9799
|
+
text
|
|
9800
|
+
}) !== null;
|
|
9818
9801
|
}
|
|
9819
9802
|
function telegramErrorMessage(error) {
|
|
9820
9803
|
return error instanceof Error ? error.message : String(error);
|