context-mode 1.0.121 → 1.0.123
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +4 -4
- package/build/adapters/claude-code/hooks.d.ts +16 -1
- package/build/adapters/claude-code/hooks.js +16 -0
- package/build/adapters/claude-code/index.js +2 -11
- package/build/adapters/client-map.js +6 -0
- package/build/adapters/codex/hooks.d.ts +19 -0
- package/build/adapters/codex/hooks.js +22 -0
- package/build/adapters/codex/index.js +8 -1
- package/build/adapters/copilot-base.d.ts +17 -1
- package/build/adapters/copilot-base.js +18 -2
- package/build/adapters/cursor/hooks.d.ts +14 -1
- package/build/adapters/cursor/hooks.js +14 -0
- package/build/adapters/detect.d.ts +12 -2
- package/build/adapters/detect.js +96 -13
- package/build/adapters/gemini-cli/hooks.d.ts +16 -0
- package/build/adapters/gemini-cli/hooks.js +19 -0
- package/build/adapters/gemini-cli/index.js +4 -2
- package/build/adapters/kiro/hooks.d.ts +16 -1
- package/build/adapters/kiro/hooks.js +19 -0
- package/build/adapters/pi/extension.d.ts +9 -0
- package/build/adapters/pi/extension.js +52 -1
- package/build/adapters/qwen-code/hooks.d.ts +26 -0
- package/build/adapters/qwen-code/hooks.js +29 -0
- package/build/adapters/qwen-code/index.js +6 -0
- package/build/cli.js +46 -5
- package/build/executor.js +18 -3
- package/build/lifecycle.d.ts +15 -0
- package/build/lifecycle.js +24 -1
- package/build/runtime.js +34 -13
- package/build/server.js +17 -2
- package/build/session/extract.js +150 -48
- package/build/session/snapshot.js +46 -0
- package/cli.bundle.mjs +151 -150
- package/configs/codex/hooks.json +1 -1
- package/configs/cursor/hooks.json +1 -1
- package/configs/kiro/agent.json +1 -1
- package/hooks/core/routing.mjs +56 -1
- package/hooks/cursor/hooks.json +1 -1
- package/hooks/ensure-deps.mjs +45 -10
- package/hooks/hooks.json +9 -0
- package/hooks/routing-block.mjs +5 -0
- package/hooks/session-extract.bundle.mjs +2 -2
- package/hooks/session-snapshot.bundle.mjs +21 -20
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/scripts/heal-better-sqlite3.mjs +188 -10
- package/scripts/heal-installed-plugins.mjs +111 -0
- package/scripts/postinstall.mjs +35 -9
- package/server.bundle.mjs +118 -118
- package/start.mjs +14 -1
- package/.mcp.json +0 -8
package/build/session/extract.js
CHANGED
|
@@ -704,17 +704,38 @@ function extractWorktree(input) {
|
|
|
704
704
|
/**
|
|
705
705
|
* Category 6: decision
|
|
706
706
|
* User corrections / approach selections.
|
|
707
|
+
*
|
|
708
|
+
* Universal-rule detector (Hybrid C, issue #535):
|
|
709
|
+
* A decision message typically takes the structural shape
|
|
710
|
+
* "{negation/rejection} X {separator} Y" — across every human language.
|
|
711
|
+
*
|
|
712
|
+
* We treat the following as the structural shape:
|
|
713
|
+
* - contains a clause separator (ASCII `,` `;`, fullwidth `,` `;`,
|
|
714
|
+
* Japanese ideographic `、`, Arabic `،`), AND
|
|
715
|
+
* - codepoint length is in the corrective range (15..500), AND
|
|
716
|
+
* - the message is not a question (no cross-script `?`), AND
|
|
717
|
+
* - contains at least one alphabetic codepoint.
|
|
718
|
+
*
|
|
719
|
+
* The renderer prints the raw message back to the next LLM, so the gate
|
|
720
|
+
* only needs to be a coarse "looks like a correction" filter — the LLM
|
|
721
|
+
* handles fine-grained interpretation. No per-language keyword list.
|
|
707
722
|
*/
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
723
|
+
const CLAUSE_SEPARATOR_PATTERN = /[,;,;、،]/u;
|
|
724
|
+
const DECISION_MIN_CHARS = 15;
|
|
725
|
+
const DECISION_MAX_CHARS = 500;
|
|
726
|
+
function looksLikeDecision(trimmed) {
|
|
727
|
+
if (QUESTION_MARK_PATTERN.test(trimmed))
|
|
728
|
+
return false;
|
|
729
|
+
if (!ALPHABETIC_PATTERN.test(trimmed))
|
|
730
|
+
return false;
|
|
731
|
+
if (!CLAUSE_SEPARATOR_PATTERN.test(trimmed))
|
|
732
|
+
return false;
|
|
733
|
+
const codepointLength = [...trimmed].length;
|
|
734
|
+
return codepointLength >= DECISION_MIN_CHARS && codepointLength <= DECISION_MAX_CHARS;
|
|
735
|
+
}
|
|
715
736
|
function extractUserDecision(message) {
|
|
716
|
-
const
|
|
717
|
-
if (!
|
|
737
|
+
const trimmed = message.trim();
|
|
738
|
+
if (!looksLikeDecision(trimmed))
|
|
718
739
|
return [];
|
|
719
740
|
return [{
|
|
720
741
|
type: "decision",
|
|
@@ -726,16 +747,58 @@ function extractUserDecision(message) {
|
|
|
726
747
|
/**
|
|
727
748
|
* Category 7: role
|
|
728
749
|
* Persona / behavioral directive patterns.
|
|
750
|
+
*
|
|
751
|
+
* Universal-rule detector (Hybrid C, issue #535):
|
|
752
|
+
* A persona/role statement is structurally a single non-question clause
|
|
753
|
+
* of moderate length containing more than one lexical token — e.g.
|
|
754
|
+
* "You are a senior engineer", "Tu es développeur",
|
|
755
|
+
* "あなたは経験豊富なエンジニアです", "Sen kıdemli mühendisisin".
|
|
756
|
+
*
|
|
757
|
+
* We treat the following as the structural shape:
|
|
758
|
+
* - codepoint length is in the persona range (12..120), AND
|
|
759
|
+
* - is not a question (no cross-script `?`), AND
|
|
760
|
+
* - is a single clause (no clause separator that would mark it as a
|
|
761
|
+
* decision), AND
|
|
762
|
+
* - carries enough lexical density: either two whitespace-separated
|
|
763
|
+
* runs of letters, OR a continuous Unicode-letter run of ≥6
|
|
764
|
+
* codepoints (a fallback for scripts without word spaces — Japanese,
|
|
765
|
+
* Chinese, Thai).
|
|
766
|
+
*
|
|
767
|
+
* The renderer prints the raw message back to the next LLM verbatim,
|
|
768
|
+
* so the gate only needs a coarse "looks like a persona statement"
|
|
769
|
+
* filter — no per-language keyword list.
|
|
729
770
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
771
|
+
// Lower bound accommodates information-dense scripts (Chinese, Japanese,
|
|
772
|
+
// Korean) where a complete persona sentence may use as few as 8 codepoints
|
|
773
|
+
// — e.g. "你是高级工程师" — while still excluding bare single-token noise.
|
|
774
|
+
const ROLE_MIN_CHARS = 8;
|
|
775
|
+
const ROLE_MAX_CHARS = 120;
|
|
776
|
+
const TWO_LEXICAL_TOKENS_PATTERN = /\p{L}+\s+\p{L}+/u;
|
|
777
|
+
const CONTINUOUS_LETTER_RUN_PATTERN = /\p{L}{6,}/u;
|
|
778
|
+
function looksLikeRole(trimmed) {
|
|
779
|
+
// Role prompts are persona-prefix shaped: the FIRST SENTENCE declares the
|
|
780
|
+
// role (e.g. "You are a senior backend engineer. <long context...>").
|
|
781
|
+
// Apply the structural test to the first clause only — real-world role
|
|
782
|
+
// prompts often append context paragraphs that would blow the length cap
|
|
783
|
+
// if we tested the whole message. First-clause shape is the load-bearing
|
|
784
|
+
// signal across languages (English "You are X.", French "Tu es X.",
|
|
785
|
+
// Japanese "あなたは X です。" all parse the same way under a period split).
|
|
786
|
+
const firstClause = trimmed.split(/[.!\n。!]/u)[0].trim();
|
|
787
|
+
if (QUESTION_MARK_PATTERN.test(firstClause))
|
|
788
|
+
return false;
|
|
789
|
+
if (CLAUSE_SEPARATOR_PATTERN.test(firstClause))
|
|
790
|
+
return false;
|
|
791
|
+
if (!ALPHABETIC_PATTERN.test(firstClause))
|
|
792
|
+
return false;
|
|
793
|
+
const codepointLength = [...firstClause].length;
|
|
794
|
+
if (codepointLength < ROLE_MIN_CHARS || codepointLength > ROLE_MAX_CHARS)
|
|
795
|
+
return false;
|
|
796
|
+
return (TWO_LEXICAL_TOKENS_PATTERN.test(firstClause) ||
|
|
797
|
+
CONTINUOUS_LETTER_RUN_PATTERN.test(firstClause));
|
|
798
|
+
}
|
|
736
799
|
function extractRole(message) {
|
|
737
|
-
const
|
|
738
|
-
if (!
|
|
800
|
+
const trimmed = message.trim();
|
|
801
|
+
if (!looksLikeRole(trimmed))
|
|
739
802
|
return [];
|
|
740
803
|
return [{
|
|
741
804
|
type: "role",
|
|
@@ -747,50 +810,90 @@ function extractRole(message) {
|
|
|
747
810
|
/**
|
|
748
811
|
* Category 13: intent
|
|
749
812
|
* Session mode classification from user messages.
|
|
813
|
+
*
|
|
814
|
+
* Universal-rule detector (Hybrid C, issue #535):
|
|
815
|
+
* investigate — message contains a question mark from any script:
|
|
816
|
+
* ASCII `?` U+003F, fullwidth `?` U+FF1F, Arabic `؟` U+061F,
|
|
817
|
+
* Spanish opening `¿` U+00BF.
|
|
818
|
+
* (Greek `;` U+037E and Armenian `՞` U+055E are excluded —
|
|
819
|
+
* Greek shares its codepoint with ASCII semicolon, which
|
|
820
|
+
* would produce false positives across the corpus.)
|
|
821
|
+
*
|
|
822
|
+
* Structural / Unicode-aware — no per-language keyword list.
|
|
750
823
|
*/
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
824
|
+
const QUESTION_MARK_PATTERN = /[??؟¿]/u;
|
|
825
|
+
/**
|
|
826
|
+
* "Imperative tone" structural heuristic for implement intent:
|
|
827
|
+
* - trimmed length < IMPERATIVE_MAX_CHARS codepoints (short directive,
|
|
828
|
+
* not a discursive paragraph)
|
|
829
|
+
* - contains no question mark from any script
|
|
830
|
+
* - contains at least one alphabetic codepoint (filters pure punctuation noise)
|
|
831
|
+
*
|
|
832
|
+
* `[...str]` walks Unicode codepoints so CJK / Indic scripts are measured
|
|
833
|
+
* fairly against the budget rather than penalised by UTF-16 unit count.
|
|
834
|
+
*/
|
|
835
|
+
const ALPHABETIC_PATTERN = /\p{L}/u;
|
|
836
|
+
const IMPERATIVE_MAX_CHARS = 60;
|
|
837
|
+
function isImperativeTone(trimmed) {
|
|
838
|
+
if (QUESTION_MARK_PATTERN.test(trimmed))
|
|
839
|
+
return false;
|
|
840
|
+
if (!ALPHABETIC_PATTERN.test(trimmed))
|
|
841
|
+
return false;
|
|
842
|
+
const codepointLength = [...trimmed].length;
|
|
843
|
+
return codepointLength > 0 && codepointLength < IMPERATIVE_MAX_CHARS;
|
|
844
|
+
}
|
|
757
845
|
function extractIntent(message) {
|
|
758
|
-
const
|
|
759
|
-
if (!
|
|
846
|
+
const trimmed = message.trim();
|
|
847
|
+
if (!trimmed)
|
|
848
|
+
return [];
|
|
849
|
+
let mode;
|
|
850
|
+
if (QUESTION_MARK_PATTERN.test(trimmed)) {
|
|
851
|
+
mode = "investigate";
|
|
852
|
+
}
|
|
853
|
+
else if (isImperativeTone(trimmed)) {
|
|
854
|
+
mode = "implement";
|
|
855
|
+
}
|
|
856
|
+
if (!mode)
|
|
760
857
|
return [];
|
|
761
858
|
return [{
|
|
762
859
|
type: "intent",
|
|
763
860
|
category: "intent",
|
|
764
|
-
data: safeString(
|
|
861
|
+
data: safeString(mode),
|
|
765
862
|
priority: 4,
|
|
766
863
|
}];
|
|
767
864
|
}
|
|
768
865
|
/**
|
|
769
866
|
* Category 25: blocked-on
|
|
770
867
|
* Detect when work is blocked on something, or when a blocker is resolved.
|
|
868
|
+
*
|
|
869
|
+
* Universal-rule detector (Hybrid C, issue #535):
|
|
870
|
+
* Programming-domain error markers are script-agnostic — they are
|
|
871
|
+
* emitted by tooling regardless of the user's spoken language. The
|
|
872
|
+
* words "Error", "Exception", "Traceback" stay in their original
|
|
873
|
+
* English form inside a Chinese / Arabic / Russian terminal log.
|
|
874
|
+
*
|
|
875
|
+
* blocker matches:
|
|
876
|
+
* - the literal "Error:" / "Exception:" / "Traceback" tokens, OR
|
|
877
|
+
* - a Python-style frame line ("File ", `line:col`), OR
|
|
878
|
+
* - a JS / Java-style stack frame ("at <ident>(...)" with a
|
|
879
|
+
* `:line:col` suffix).
|
|
880
|
+
*
|
|
881
|
+
* blocker_resolved matches:
|
|
882
|
+
* - a Unicode check-mark glyph (✓ U+2713, ✔ U+2714, ✅ U+2705,
|
|
883
|
+
* ☑ U+2611, 🎉 U+1F389), OR
|
|
884
|
+
* - the structural marker "fixed: …" / "resolved: …" — these are
|
|
885
|
+
* programming-domain conventions (git log, PR titles, CHANGELOG
|
|
886
|
+
* entries) rather than natural-language phrases.
|
|
771
887
|
*/
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
/\bneed\s+\S+\s+before\b/i,
|
|
776
|
-
/\bcan'?t proceed until\b/i,
|
|
777
|
-
/\bdepends on\b/i,
|
|
778
|
-
/\bblocked\b/i,
|
|
779
|
-
// Turkish patterns
|
|
780
|
-
/\bbekliyor\b/i,
|
|
781
|
-
/\bbekliyorum\b/i,
|
|
782
|
-
];
|
|
783
|
-
const BLOCKER_RESOLVED_PATTERNS = [
|
|
784
|
-
/\bunblocked\b/i,
|
|
785
|
-
/\bresolved\b/i,
|
|
786
|
-
/\bgot the\s+\S+/i,
|
|
787
|
-
/\bis ready now\b/i,
|
|
788
|
-
/\bcan proceed\b/i,
|
|
789
|
-
];
|
|
888
|
+
const BLOCKER_MARKERS_PATTERN = /(?:\bError\s*:|\bException\s*:|\bTraceback\b|\bat\s+\S+\s*\([^)]*:\d+:\d+\))/u;
|
|
889
|
+
const BLOCKER_RESOLVED_CHECKMARK_PATTERN = /[✓✔✅☑🎉]/u;
|
|
890
|
+
const BLOCKER_RESOLVED_MARKER_PATTERN = /^\s*(?:fixed|resolved)\s*:/iu;
|
|
790
891
|
function extractBlocker(message) {
|
|
791
892
|
const events = [];
|
|
792
|
-
//
|
|
793
|
-
|
|
893
|
+
// Resolution takes precedence — if both shapes match, render the
|
|
894
|
+
// happier signal so the snapshot reflects the latest state.
|
|
895
|
+
const isResolved = BLOCKER_RESOLVED_CHECKMARK_PATTERN.test(message) ||
|
|
896
|
+
BLOCKER_RESOLVED_MARKER_PATTERN.test(message);
|
|
794
897
|
if (isResolved) {
|
|
795
898
|
events.push({
|
|
796
899
|
type: "blocker_resolved",
|
|
@@ -800,8 +903,7 @@ function extractBlocker(message) {
|
|
|
800
903
|
});
|
|
801
904
|
return events;
|
|
802
905
|
}
|
|
803
|
-
|
|
804
|
-
if (isBlocked) {
|
|
906
|
+
if (BLOCKER_MARKERS_PATTERN.test(message)) {
|
|
805
907
|
events.push({
|
|
806
908
|
type: "blocker",
|
|
807
909
|
category: "blocked-on",
|
|
@@ -342,6 +342,43 @@ function buildIntentSection(intentEvents) {
|
|
|
342
342
|
const lastIntent = intentEvents[intentEvents.length - 1];
|
|
343
343
|
return ` <intent mode="${escapeXML(lastIntent.data)}"/>`;
|
|
344
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Raw-prompt safety net (issue #535):
|
|
347
|
+
* Always surface the most recent user prompts verbatim so the next LLM
|
|
348
|
+
* sees them even if every universal-rule detector misses. Bound per-prompt
|
|
349
|
+
* payload to RECENT_MESSAGE_MAX_CHARS Unicode codepoints; bound the total
|
|
350
|
+
* count to RECENT_MESSAGES_LIMIT to keep the resume block compact.
|
|
351
|
+
*/
|
|
352
|
+
const RECENT_MESSAGES_LIMIT = 3;
|
|
353
|
+
const RECENT_MESSAGE_MAX_CHARS = 400;
|
|
354
|
+
function truncateForSnapshot(value, max) {
|
|
355
|
+
const codepoints = [...value];
|
|
356
|
+
if (codepoints.length <= max)
|
|
357
|
+
return value;
|
|
358
|
+
return codepoints.slice(0, max).join("");
|
|
359
|
+
}
|
|
360
|
+
function buildRecentMessagesSection(userPromptEvents) {
|
|
361
|
+
if (userPromptEvents.length === 0)
|
|
362
|
+
return "";
|
|
363
|
+
// Last N in chronological order — newest at the bottom mirrors the
|
|
364
|
+
// way the user reads their own scrollback.
|
|
365
|
+
const recent = userPromptEvents.slice(-RECENT_MESSAGES_LIMIT);
|
|
366
|
+
const items = recent
|
|
367
|
+
.map(ev => {
|
|
368
|
+
const body = truncateForSnapshot(ev.data ?? "", RECENT_MESSAGE_MAX_CHARS);
|
|
369
|
+
if (!body)
|
|
370
|
+
return "";
|
|
371
|
+
return ` <message>${escapeXML(body)}</message>`;
|
|
372
|
+
})
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
if (items.length === 0)
|
|
375
|
+
return "";
|
|
376
|
+
return [
|
|
377
|
+
` <recent_user_messages count="${items.length}">`,
|
|
378
|
+
...items,
|
|
379
|
+
` </recent_user_messages>`,
|
|
380
|
+
].join("\n");
|
|
381
|
+
}
|
|
345
382
|
// ── Main builder ─────────────────────────────────────────────────────────────
|
|
346
383
|
/**
|
|
347
384
|
* Build a reference-based resume snapshot XML string from stored session events.
|
|
@@ -369,6 +406,7 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
369
406
|
const intentEvents = [];
|
|
370
407
|
const skillEvents = [];
|
|
371
408
|
const roleEvents = [];
|
|
409
|
+
const userPromptEvents = [];
|
|
372
410
|
for (const ev of events) {
|
|
373
411
|
switch (ev.category) {
|
|
374
412
|
case "file":
|
|
@@ -407,6 +445,9 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
407
445
|
case "role":
|
|
408
446
|
roleEvents.push(ev);
|
|
409
447
|
break;
|
|
448
|
+
case "user-prompt":
|
|
449
|
+
userPromptEvents.push(ev);
|
|
450
|
+
break;
|
|
410
451
|
}
|
|
411
452
|
}
|
|
412
453
|
// ── Build all sections ──
|
|
@@ -451,6 +492,11 @@ export function buildResumeSnapshot(events, opts) {
|
|
|
451
492
|
const intent = buildIntentSection(intentEvents);
|
|
452
493
|
if (intent)
|
|
453
494
|
sections.push(intent);
|
|
495
|
+
// Raw-prompt safety net — always last so it stays adjacent to the next
|
|
496
|
+
// LLM turn and is read after the structured sections.
|
|
497
|
+
const recentMessages = buildRecentMessagesSection(userPromptEvents);
|
|
498
|
+
if (recentMessages)
|
|
499
|
+
sections.push(recentMessages);
|
|
454
500
|
// ── Assemble ──
|
|
455
501
|
const header = `<session_resume events="${events.length}" compact_count="${compactCount}" generated_at="${now}">`;
|
|
456
502
|
const footer = `</session_resume>`;
|