hippo-memory 1.63.2 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/agent-memories/claude-code.js +3 -3
- package/dist/ambient-store.d.ts +2 -0
- package/dist/ambient-store.js +4 -3
- package/dist/api/assemble.d.ts +2 -0
- package/dist/api/assemble.js +11 -7
- package/dist/api/auth.d.ts +15 -11
- package/dist/api/auth.js +116 -22
- package/dist/api/context-select.d.ts +5 -3
- package/dist/api/context-select.js +6 -11
- package/dist/api/context-types.d.ts +2 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.js +79 -55
- package/dist/api/dormant.d.ts +5 -5
- package/dist/api/dormant.js +13 -9
- package/dist/api/drill-down.d.ts +2 -0
- package/dist/api/drill-down.js +12 -7
- package/dist/api/forget.d.ts +1 -1
- package/dist/api/forget.js +8 -4
- package/dist/api/learn.js +5 -3
- package/dist/api/outcome.d.ts +1 -1
- package/dist/api/outcome.js +2 -1
- package/dist/api/promote.js +8 -5
- package/dist/api/recall-types.d.ts +4 -2
- package/dist/api/recall.js +51 -36
- package/dist/api/remember.d.ts +7 -0
- package/dist/api/remember.js +26 -2
- package/dist/api/sleep-run.d.ts +28 -0
- package/dist/api/sleep-run.js +267 -0
- package/dist/api/sleep.d.ts +5 -45
- package/dist/api/sleep.js +5 -252
- package/dist/api/types.d.ts +5 -0
- package/dist/api/types.js +4 -0
- package/dist/audit-prune.js +1 -1
- package/dist/audit.d.ts +1 -3
- package/dist/audit.js +7 -103
- package/dist/auth.d.ts +48 -4
- package/dist/auth.js +111 -42
- package/dist/autolearn.js +2 -2
- package/dist/capture/caller-session.d.ts +16 -0
- package/dist/capture/caller-session.js +58 -0
- package/dist/capture/command.d.ts +19 -1
- package/dist/capture/command.js +28 -21
- package/dist/capture/compact-resume-caller.d.ts +11 -0
- package/dist/capture/compact-resume-caller.js +35 -0
- package/dist/capture/compact.d.ts +0 -14
- package/dist/capture/compact.js +7 -160
- package/dist/capture/compaction-items-caller.d.ts +16 -0
- package/dist/capture/compaction-items-caller.js +32 -0
- package/dist/capture/extract.d.ts +2 -14
- package/dist/capture/extract.js +135 -346
- package/dist/capture/failure-caller.d.ts +21 -0
- package/dist/capture/failure-caller.js +74 -0
- package/dist/capture/failure-reading.d.ts +44 -0
- package/dist/capture/failure-reading.js +78 -0
- package/dist/capture/pre-compact-caller.d.ts +16 -0
- package/dist/capture/pre-compact-caller.js +39 -0
- package/dist/capture/session-end-caller.d.ts +21 -0
- package/dist/capture/session-end-caller.js +30 -0
- package/dist/capture/session-texts.d.ts +14 -0
- package/dist/capture/session-texts.js +42 -0
- package/dist/capture/transcript.d.ts +6 -1
- package/dist/capture/transcript.js +20 -16
- package/dist/capture/working-state.d.ts +21 -0
- package/dist/capture/working-state.js +154 -0
- package/dist/capture-error.d.ts +8 -22
- package/dist/capture-error.js +9 -61
- package/dist/cli/audit.js +25 -0
- package/dist/cli/auth.js +4 -3
- package/dist/cli/context.js +13 -158
- package/dist/cli/continuity.js +2 -1
- package/dist/cli/init.js +5 -3
- package/dist/cli/last-sleep.d.ts +6 -0
- package/dist/cli/last-sleep.js +68 -0
- package/dist/cli/quality-repair-once.d.ts +3 -0
- package/dist/cli/quality-repair-once.js +36 -0
- package/dist/cli/recall.js +2 -1
- package/dist/cli/remember.d.ts +2 -0
- package/dist/cli/remember.js +4 -1
- package/dist/cli/session-hooks.d.ts +0 -3
- package/dist/cli/session-hooks.js +12 -49
- package/dist/cli/setup.js +10 -2
- package/dist/cli/shared.d.ts +7 -14
- package/dist/cli/shared.js +19 -78
- package/dist/cli/sleep.js +6 -3
- package/dist/cli/usage.js +18 -9
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -2
- package/dist/client.js +1 -1
- package/dist/compaction-record.d.ts +19 -1
- package/dist/compaction-record.js +61 -132
- package/dist/compaction-spool.d.ts +45 -0
- package/dist/compaction-spool.js +454 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +49 -0
- package/dist/connectors/github/deletion.d.ts +1 -1
- package/dist/connectors/github/deletion.js +5 -17
- package/dist/consolidate/conflicts.js +4 -0
- package/dist/consolidate/flush-units.d.ts +9 -0
- package/dist/consolidate/flush-units.js +63 -0
- package/dist/consolidate/llm-passes.js +5 -4
- package/dist/consolidate/merge.js +5 -1
- package/dist/consolidate/run.d.ts +2 -1
- package/dist/consolidate/sleep.d.ts +2 -0
- package/dist/consolidate/sleep.js +82 -15
- package/dist/context-render.d.ts +14 -0
- package/dist/context-render.js +28 -0
- package/dist/dag.js +6 -0
- package/dist/dashboard-actions.js +3 -2
- package/dist/dashboard.js +4 -3
- package/dist/db/backup.d.ts +6 -0
- package/dist/db/backup.js +29 -0
- package/dist/db/busy.d.ts +9 -0
- package/dist/db/busy.js +35 -0
- package/dist/db/continuity.d.ts +1 -0
- package/dist/db/continuity.js +31 -4
- package/dist/db/meta.d.ts +2 -0
- package/dist/db/meta.js +7 -0
- package/dist/db/migrate.js +2 -3
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +5 -1
- package/dist/db/migrations/v39.js +2 -7
- package/dist/db/migrations/v53.d.ts +3 -0
- package/dist/db/migrations/v53.js +16 -0
- package/dist/db/migrations/v54.d.ts +3 -0
- package/dist/db/migrations/v54.js +25 -0
- package/dist/db/open.d.ts +11 -0
- package/dist/db/open.js +27 -0
- package/dist/db/tables.d.ts +2 -0
- package/dist/db/tables.js +6 -2
- package/dist/db.d.ts +2 -2
- package/dist/db.js +2 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +31 -15
- package/dist/dormant.d.ts +16 -20
- package/dist/dormant.js +24 -30
- package/dist/entry/json-hooks.d.ts +3 -0
- package/dist/entry/json-hooks.js +4 -0
- package/dist/entry/project-identity.d.ts +2 -0
- package/dist/entry/project-identity.js +3 -0
- package/dist/entry/session-text.d.ts +7 -0
- package/dist/entry/session-text.js +8 -0
- package/dist/extract.js +6 -0
- package/dist/failure-log.d.ts +10 -2
- package/dist/failure-log.js +19 -2
- package/dist/gated-write.js +1 -1
- package/dist/graph/delta.d.ts +44 -0
- package/dist/graph/delta.js +109 -0
- package/dist/graph/types.d.ts +5 -0
- package/dist/graph/write.d.ts +20 -20
- package/dist/graph/write.js +103 -36
- package/dist/graph-extract.d.ts +43 -9
- package/dist/graph-extract.js +140 -157
- package/dist/graph-recall.d.ts +1 -0
- package/dist/graph-recall.js +2 -2
- package/dist/graph-view.d.ts +1 -0
- package/dist/graph-view.js +25 -2
- package/dist/half-life-migration.js +5 -1
- package/dist/handoff-evidence.d.ts +5 -0
- package/dist/handoff-evidence.js +30 -0
- package/dist/home-path.d.ts +5 -0
- package/dist/home-path.js +24 -0
- package/dist/hooks/json-hooks.d.ts +13 -2
- package/dist/hooks/json-hooks.js +246 -117
- package/dist/hooks/opencode.js +1 -1
- package/dist/hooks/shared.d.ts +2 -0
- package/dist/hooks/shared.js +6 -1
- package/dist/http-util.d.ts +16 -7
- package/dist/http-util.js +65 -20
- package/dist/importers/vault.js +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/json.d.ts +2 -0
- package/dist/json.js +7 -0
- package/dist/ledger-db.d.ts +10 -0
- package/dist/ledger-db.js +42 -0
- package/dist/mcp/admin-tools.d.ts +2 -2
- package/dist/mcp/admin-tools.js +47 -5
- package/dist/mcp/memory-tools.js +7 -2
- package/dist/mcp/protocol.d.ts +6 -0
- package/dist/mcp/protocol.js +4 -2
- package/dist/mcp/recall-tools.js +19 -6
- package/dist/mcp/request.js +4 -0
- package/dist/mcp/session-state.d.ts +4 -0
- package/dist/mcp/session-state.js +15 -6
- package/dist/mcp/shared-gate.d.ts +4 -0
- package/dist/mcp/shared-gate.js +22 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/memory-quality.d.ts +35 -0
- package/dist/memory-quality.js +191 -0
- package/dist/memory.d.ts +4 -4
- package/dist/physics-state.js +1 -1
- package/dist/pilot-arm.d.ts +14 -3
- package/dist/pilot-arm.js +32 -10
- package/dist/postinstall.js +2 -2
- package/dist/project-briefs.js +4 -4
- package/dist/project-identity.d.ts +14 -10
- package/dist/project-identity.js +29 -11
- package/dist/project-merge.d.ts +0 -2
- package/dist/project-merge.js +9 -13
- package/dist/prompt-hook.d.ts +65 -0
- package/dist/prompt-hook.js +213 -0
- package/dist/prompt-recall.js +1 -1
- package/dist/quality-repair.d.ts +27 -0
- package/dist/quality-repair.js +220 -0
- package/dist/rate-limit.d.ts +1 -0
- package/dist/rate-limit.js +5 -3
- package/dist/raw-archive.d.ts +1 -1
- package/dist/raw-archive.js +6 -16
- package/dist/recall-scope.d.ts +28 -5
- package/dist/recall-scope.js +52 -7
- package/dist/recall-trace.js +1 -1
- package/dist/reject-flow.d.ts +2 -0
- package/dist/reject-flow.js +20 -6
- package/dist/secret-detect.js +7 -8
- package/dist/server/auth.d.ts +8 -6
- package/dist/server/auth.js +85 -38
- package/dist/server/client-ip.d.ts +2 -0
- package/dist/server/client-ip.js +16 -1
- package/dist/server/mcp-http.d.ts +9 -3
- package/dist/server/mcp-http.js +56 -13
- package/dist/server/request.d.ts +1 -1
- package/dist/server/request.js +5 -5
- package/dist/server/routes/admin.js +12 -6
- package/dist/server/routes/memories.js +9 -1
- package/dist/server/routes/recall.js +14 -2
- package/dist/server/types.d.ts +42 -5
- package/dist/server/validation.d.ts +5 -2
- package/dist/server/validation.js +19 -2
- package/dist/server.d.ts +21 -4
- package/dist/server.js +207 -49
- package/dist/session-digest.d.ts +0 -1
- package/dist/session-digest.js +3 -12
- package/dist/session-owners.d.ts +7 -0
- package/dist/session-owners.js +54 -0
- package/dist/share-scrub.d.ts +3 -0
- package/dist/share-scrub.js +8 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +26 -18
- package/dist/store/candidates.d.ts +14 -3
- package/dist/store/candidates.js +48 -26
- package/dist/store/conflicts.d.ts +15 -0
- package/dist/store/conflicts.js +68 -44
- package/dist/store/delete-and-batch.d.ts +25 -1
- package/dist/store/delete-and-batch.js +121 -69
- package/dist/store/entry-reads.d.ts +5 -6
- package/dist/store/entry-reads.js +24 -12
- package/dist/store/entry-row.d.ts +7 -10
- package/dist/store/entry-row.js +40 -18
- package/dist/store/entry-writes.d.ts +3 -3
- package/dist/store/entry-writes.js +9 -23
- package/dist/store/handoffs.d.ts +9 -6
- package/dist/store/handoffs.js +72 -59
- package/dist/store/index-and-stats.js +2 -2
- package/dist/store/open.d.ts +3 -1
- package/dist/store/open.js +3 -3
- package/dist/store/search-rows.d.ts +8 -5
- package/dist/store/search-rows.js +15 -18
- package/dist/store/sessions.d.ts +25 -6
- package/dist/store/sessions.js +61 -19
- package/dist/store/summaries.d.ts +1 -1
- package/dist/store/summaries.js +3 -18
- package/dist/store/tenant-lookup.d.ts +7 -3
- package/dist/store/tenant-lookup.js +9 -9
- package/dist/store-port.d.ts +13 -0
- package/dist/store-port.js +20 -0
- package/dist/version.d.ts +5 -1
- package/dist/version.js +5 -1
- package/dist/working-memory.js +1 -1
- package/dist/write-budget.d.ts +15 -0
- package/dist/write-budget.js +15 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -4
package/dist/capture/extract.js
CHANGED
|
@@ -1,386 +1,175 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Capture actionable items from conversation text.
|
|
3
|
-
*
|
|
4
|
-
* Uses heuristic pattern matching (no LLM) to extract:
|
|
5
|
-
* - Decisions ("we decided", "let's do", "going with")
|
|
6
|
-
* - Specs / requirements (bullet lists after spec/feature/plan headings)
|
|
7
|
-
* - Rules / constraints ("never", "always", "the rule is", "must")
|
|
8
|
-
* - Errors / gotchas ("error:", "bug:", "gotcha:", "watch out")
|
|
9
|
-
* - Preferences ("prefer", "use X instead of Y", "don't use")
|
|
10
|
-
*/
|
|
11
1
|
import { duplicateKey } from '../same-text.js';
|
|
12
|
-
import {
|
|
13
|
-
// Sentence-level patterns
|
|
14
|
-
//
|
|
15
|
-
// Each pattern carries TWO capture groups: group 1 is the discriminating keyword (plus its
|
|
16
|
-
// trailing separator, verbatim), group 2 the content after it, so a negation like "never" survives
|
|
17
|
-
// ("Never use X" must not store as "use X"). `extractFromPatterns` joins group1 + a clause-bounded
|
|
18
|
-
// group2 (`boundToClause`), so group 2 may reach {1,500}. The keyword keeps its own group because
|
|
19
|
-
// keywords like "error:" end in a colon that is not a prose clause boundary.
|
|
20
|
-
// `PREFERENCE_PATTERNS[0]` is the exception: its two groups are the content spans either side of
|
|
21
|
-
// "instead of"/"over"/"not"; left as-is.
|
|
2
|
+
import { assessAutomaticMemory } from '../memory-quality.js';
|
|
22
3
|
const DECISION_PATTERNS = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/(decision:\s*)(.{1,500})/i,
|
|
4
|
+
/\b(?:we(?:'ve| have)?|i(?:'ve| have)?|let's)\s+decid(?:ed|e)\b/i,
|
|
5
|
+
/\b(?:let's|we(?:'ll| will| should)?)\s+(?:go with|do|use|try|build|implement|switch to)\b/i,
|
|
6
|
+
/\b(?:going|went)\s+with\b/i,
|
|
7
|
+
/\b(?:the plan is|plan:|decision:)\s/i,
|
|
28
8
|
];
|
|
29
9
|
const RULE_PATTERNS = [
|
|
30
|
-
/\b(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/((?:make sure|ensure)\s+(?:to\s+)?)(.{1,500})/i,
|
|
10
|
+
/\b(?:never|always|must(?:\s+not)?|do(?:n't| not)\s+ever)\s/i,
|
|
11
|
+
/\b(?:the rule is|rule:|important:|critical:|remember:)\s/i,
|
|
12
|
+
/\b(?:make sure|ensure)\s/i,
|
|
34
13
|
];
|
|
35
14
|
const ERROR_PATTERNS = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
/\b(?:error|bug|gotcha|watch out|careful|warning|caveat|trap):\s/i,
|
|
16
|
+
/\b(?:this broke|this breaks|this will break|broke because)\s/i,
|
|
17
|
+
/\bthe (?:issue|problem|fix) (?:is|was)\s/i,
|
|
18
|
+
/\b(?:don't forget|easy to miss):\s/i,
|
|
40
19
|
];
|
|
41
|
-
// PREFERENCE_PATTERNS[0] keeps its match[1]-only, unbounded shape; extractFromPatterns
|
|
42
|
-
// checks for this exact array element.
|
|
43
20
|
const PREFERENCE_PATTERNS = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
21
|
+
/\b(?:prefer|use)\s+.+\s+(?:instead of|over|not)\s+/i,
|
|
22
|
+
/\b(?:prefer|don't use|avoid|skip)\s/i,
|
|
23
|
+
/\b(?:we(?:'re| are)\s+using|the stack is|we use)\s/i,
|
|
47
24
|
];
|
|
48
|
-
|
|
25
|
+
const CATEGORIES = [
|
|
26
|
+
['decision', DECISION_PATTERNS], ['rule', RULE_PATTERNS],
|
|
27
|
+
['error', ERROR_PATTERNS], ['preference', PREFERENCE_PATTERNS],
|
|
28
|
+
];
|
|
29
|
+
const MAX_CHARS = 500;
|
|
30
|
+
const BULLET = /^(?:[-*]|\d+\.)\s+(?:\[[ xX]\]\s+)?(.+)/;
|
|
31
|
+
const LABEL = /^(?:decision|rule|error|bug|gotcha|important|critical|remember|plan):/i;
|
|
32
|
+
const SENTENCE_END = /[.!?]["')\]]*$/;
|
|
49
33
|
const SPEC_HEADING_PATTERNS = [
|
|
50
34
|
/^#+\s*(?:features?|requirements?|specs?|specifications?|plan|design|architecture|interface|api|todo|tasks?|implementation|notes?)(?:\s|:|$)/i,
|
|
51
35
|
/^(?:features?|requirements?|specs?|specifications?|plan|design|tasks?|implementation)(?:\s*:|$)/i,
|
|
52
36
|
];
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
37
|
+
// A list item may open a fence on its own line: "- ```".
|
|
38
|
+
const FENCE_OPEN = /^(?:(?:[-*]|\d+\.)\s+)?(`{3,}|~{3,})(.*)$/;
|
|
39
|
+
const FENCE_CLOSE = /^(`{3,}|~{3,})\s*$/;
|
|
40
|
+
// Inline code and abbreviation dots never end a sentence; "etc." does when a capital follows.
|
|
41
|
+
const NO_BREAK = /(`+)[^`\n]+\1|\b(?:[Ee]\.g|[Ii]\.e|[Vv]s|[Cc]f)\.|\betc\.(?=\s+[a-z])/g;
|
|
42
|
+
/** CommonMark fences: a backtick run with a backtick after it is inline code, and only a bare run as long closes. */
|
|
43
|
+
function proseOnly(text) {
|
|
44
|
+
let fence = null;
|
|
45
|
+
let afterProse = false;
|
|
46
|
+
return text.split('\n').map(line => {
|
|
47
|
+
const trimmed = line.trimStart();
|
|
48
|
+
if (fence !== null) {
|
|
49
|
+
const close = trimmed.match(FENCE_CLOSE);
|
|
50
|
+
if (close && close[1][0] === fence[0] && close[1].length >= fence.length)
|
|
51
|
+
fence = null;
|
|
52
|
+
afterProse = false;
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
const open = trimmed.match(FENCE_OPEN);
|
|
56
|
+
if (open && !(open[1][0] === '`' && open[2].includes('`'))) {
|
|
57
|
+
fence = open[1];
|
|
58
|
+
afterProse = false;
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
// Quotes, table rows and indented code are not the speaker's own; indented code cannot interrupt a paragraph or list item.
|
|
62
|
+
const dropped = /^\s*[>|]/.test(line) || (!afterProse && /^(?: {4}|\t)(?!\s*(?:[-*]|\d+\.)\s)/.test(line));
|
|
63
|
+
afterProse = !dropped && trimmed !== '' && !/^#{1,6}(?:\s|$)/.test(trimmed);
|
|
64
|
+
return dropped ? '' : line;
|
|
65
|
+
}).join('\n');
|
|
62
66
|
}
|
|
63
|
-
function
|
|
64
|
-
return
|
|
67
|
+
function masked(text) {
|
|
68
|
+
return text.replace(NO_BREAK, span => 'x'.repeat(span.length));
|
|
65
69
|
}
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
// A quote CLOSES THE SIDE IT IS TIGHT AGAINST, so the test needs both
|
|
73
|
-
// neighbours: these two have the same following character and opposite roles -
|
|
74
|
-
// "preserve 'a, b'-style" next '-' -> CLOSER, content on the left
|
|
75
|
-
// "then run '--force," next '-' -> OPENER, content on the right
|
|
76
|
-
// so no forward-only rule can separate them.
|
|
77
|
-
//
|
|
78
|
-
// never a closer next is a letter/digit "user's", "keep 'em"
|
|
79
|
-
// closer prev is non-whitespace "'a, b' to", "'a, b'-style"
|
|
80
|
-
// closer next is whitespace or end "preserve 'a, b ' exactly"
|
|
81
|
-
// closer next is clause punctuation "preserve 'a, b ', then"
|
|
82
|
-
// that itself ends the token
|
|
83
|
-
//
|
|
84
|
-
// The last clause is what separates ", " from ".env": a dot followed by a
|
|
85
|
-
// letter joins a filename, a dot followed by space ends a sentence.
|
|
86
|
-
for (let j = full.length - 1; j >= 0; j--) {
|
|
87
|
-
if (full[j] !== "'")
|
|
88
|
-
continue;
|
|
89
|
-
const prev = full[j - 1];
|
|
90
|
-
const next = full[j + 1];
|
|
91
|
-
if (isLetterOrDigit(next))
|
|
92
|
-
continue;
|
|
93
|
-
// "tight against content" excludes an OPENING delimiter: in
|
|
94
|
-
// "call parse('--force" the paren is non-whitespace but the quote after
|
|
95
|
-
// it is an opener, and treating it as a closer let an earlier elision
|
|
96
|
-
// pair across the clause boundary.
|
|
97
|
-
const tightBefore = prev !== undefined && !/[\s([{]/.test(prev);
|
|
98
|
-
const endsAfter = next === undefined || /\s/.test(next);
|
|
99
|
-
// Punctuation splits in two:
|
|
100
|
-
// , ; : ! ? ) ] } never join tokens - they end the literal whatever
|
|
101
|
-
// follows, so "'echo a, b ';then" closes at the ;
|
|
102
|
-
// . - _ / @ # DO join tokens - "'.env" and "'--force" continue a
|
|
103
|
-
// filename or flag, so these only close when trailed
|
|
104
|
-
// by whitespace
|
|
105
|
-
const afterNext = full[j + 2];
|
|
106
|
-
const closesRegardless = next !== undefined && /[,;:!?)\]}]/.test(next);
|
|
107
|
-
const joinerThenSpace = next !== undefined && /[.\-_/@#]/.test(next) &&
|
|
108
|
-
(afterNext === undefined || /\s/.test(afterNext));
|
|
109
|
-
if (tightBefore || endsAfter || closesRegardless || joinerThenSpace)
|
|
110
|
-
return j;
|
|
111
|
-
}
|
|
112
|
-
return -1;
|
|
70
|
+
/** A lowercase line continues the line above only when that line has not ended its sentence; "e.g." at a line end has not. */
|
|
71
|
+
function continues(line, previous) {
|
|
72
|
+
if (previous === undefined || !/^[a-z]/.test(line) || LABEL.test(line))
|
|
73
|
+
return false;
|
|
74
|
+
return !SENTENCE_END.test(masked(`${previous} ${line}`).slice(0, previous.length));
|
|
113
75
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let quote = null;
|
|
127
|
-
let cutEnd = full.length;
|
|
128
|
-
for (let i = searchFrom; i < full.length; i++) {
|
|
129
|
-
const ch = full[i];
|
|
130
|
-
if (quote) {
|
|
131
|
-
if (closesQuote(full, i, quote))
|
|
132
|
-
quote = null;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (ch === '"' || ch === '`') {
|
|
136
|
-
quote = ch;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (ch === "'") {
|
|
140
|
-
if (opensSingleQuote(full, i, lastCloser)) {
|
|
141
|
-
quote = ch;
|
|
142
|
-
}
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (ch === '(' || ch === '[' || ch === '{') {
|
|
146
|
-
depth++;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (ch === ')' || ch === ']' || ch === '}') {
|
|
150
|
-
if (depth > 0)
|
|
151
|
-
depth--;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (depth > 0)
|
|
76
|
+
function proseBlocks(text) {
|
|
77
|
+
const blocks = [];
|
|
78
|
+
let pending = [];
|
|
79
|
+
const flush = () => {
|
|
80
|
+
if (pending.length > 0)
|
|
81
|
+
blocks.push(pending.join(' '));
|
|
82
|
+
pending = [];
|
|
83
|
+
};
|
|
84
|
+
for (const line of text.split('\n')) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || /^#+\s/.test(trimmed)) {
|
|
87
|
+
flush();
|
|
155
88
|
continue;
|
|
156
|
-
const next = full[i + 1];
|
|
157
|
-
// Prose clause separator: , ; : followed by whitespace.
|
|
158
|
-
if ((ch === ',' || ch === ';' || ch === ':') && next !== undefined && /\s/.test(next)) {
|
|
159
|
-
cutEnd = i;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
// Sentence terminator, but only when followed by whitespace or end — a
|
|
163
|
-
// bare [.!?] splits inside .env, capture.ts, v1.35.0.
|
|
164
|
-
if ((ch === '.' || ch === '!' || ch === '?') && (next === undefined || /\s/.test(next))) {
|
|
165
|
-
cutEnd = i + 1;
|
|
166
|
-
break;
|
|
167
89
|
}
|
|
90
|
+
const bullet = trimmed.match(BULLET);
|
|
91
|
+
if (bullet || !continues(trimmed, pending.at(-1)))
|
|
92
|
+
flush();
|
|
93
|
+
pending.push(bullet?.[1] ?? trimmed);
|
|
168
94
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
bounded = bounded.slice(0, maxLen);
|
|
172
|
-
return bounded;
|
|
95
|
+
flush();
|
|
96
|
+
return blocks;
|
|
173
97
|
}
|
|
174
|
-
function
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
return
|
|
98
|
+
function completeSentences(text) {
|
|
99
|
+
// Masking keeps offsets, so each sentence is cut from the unchanged source.
|
|
100
|
+
const segments = new Intl.Segmenter('en', { granularity: 'sentence' }).segment(masked(text));
|
|
101
|
+
return [...segments].map(({ index, segment }) => text.slice(index, index + segment.length).trim());
|
|
178
102
|
}
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
// LITERAL, because getting either wrong reintroduces the fragment
|
|
182
|
-
// defect this change exists to remove, and both fragments PASS the
|
|
183
|
-
// write gate (code punctuation reads as "specific"):
|
|
184
|
-
// treat every ' as a quote -> "Always ensure it's enabled, then
|
|
185
|
-
// restart..." never leaves quote mode, bounding disabled entirely
|
|
186
|
-
// treat no ' as a quote -> "Always pass 'a, b' to the parser."
|
|
187
|
-
// cuts at the comma and stores "Always pass 'a"
|
|
188
|
-
//
|
|
189
|
-
// Discriminator: a ' OPENS a literal only at a word boundary - preceded
|
|
190
|
-
// by start/whitespace/open-bracket AND followed by non-whitespace. An
|
|
191
|
-
// in-word apostrophe ("it's", "user's") has letters on both sides and is
|
|
192
|
-
// just a character.
|
|
193
|
-
const prev = i > 0 ? full[i - 1] : undefined;
|
|
194
|
-
const next = full[i + 1];
|
|
195
|
-
const atBoundary = (prev === undefined || /[\s([{]/.test(prev)) &&
|
|
196
|
-
next !== undefined && !/\s/.test(next);
|
|
197
|
-
// Pairing is VERIFIED, not assumed. A word-boundary test alone still
|
|
198
|
-
// opens quote mode on elided forms ("keep 'em", "wait 'til"), which
|
|
199
|
-
// have no closer, so the scanner never leaves quote mode and bounding
|
|
200
|
-
// is disabled for the rest of the capture. Requiring an actual closing
|
|
201
|
-
// quote later in the string replaces a guess with a checkable fact -
|
|
202
|
-
// an elided form simply has no partner.
|
|
203
|
-
// ...and the partner must LOOK like a closer, not merely be another
|
|
204
|
-
// apostrophe. Distance cannot separate the two cases - both put a `'`
|
|
205
|
-
// far downstream:
|
|
206
|
-
// "Always pass '<600-char literal>' to the parser" -> the far ' IS
|
|
207
|
-
// the closer, and pairing must succeed
|
|
208
|
-
// "keep 'em enabled, then <520 chars> check user's config" -> the far
|
|
209
|
-
// ' is in-word, and pairing against it re-opens quote mode on the
|
|
210
|
-
// elision, disabling bounding for the rest of the capture
|
|
211
|
-
// So apply the SAME shape rule already used to open a literal, mirrored:
|
|
212
|
-
// a closer has non-whitespace before it and whitespace/punctuation (not
|
|
213
|
-
// a letter) after. "user's" fails it on both counts.
|
|
214
|
-
return atBoundary && lastCloser > i;
|
|
103
|
+
function insideBrackets(lead) {
|
|
104
|
+
return (lead.match(/[([{]/g) ?? []).length > (lead.match(/[)\]}]/g) ?? []).length;
|
|
215
105
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
// Trailing cleanup: clause-bounding can cut inside a parenthetical and
|
|
222
|
-
// leave an unmatched trailing ')' (e.g. "...(two had never got entries),"
|
|
223
|
-
// bounds to "never got entries)"). Strip a trailing ')' ONLY while closes
|
|
224
|
-
// outnumber opens in the string so far — a balanced parenthetical like
|
|
225
|
-
// "always run the suite (twice)" must be left intact.
|
|
226
|
-
while (content.endsWith(')')) {
|
|
227
|
-
const opens = (content.match(/\(/g) ?? []).length;
|
|
228
|
-
const closes = (content.match(/\)/g) ?? []).length;
|
|
229
|
-
if (closes <= opens)
|
|
230
|
-
break;
|
|
231
|
-
content = content
|
|
232
|
-
.slice(0, -1)
|
|
233
|
-
.replace(/[.!?,;:\s]+$/, '')
|
|
234
|
-
.trim();
|
|
235
|
-
}
|
|
236
|
-
return content;
|
|
237
|
-
}
|
|
238
|
-
/** The 500-char write gate minus the 200-char bound counted from the keyword. */
|
|
239
|
-
const RULE_LEAD_CAP = 300;
|
|
240
|
-
/** A modal rule keeps its subject ("We must never ..."); null keeps the keyword-start form when the lead is long or the keyword sits inside a bracket. */
|
|
241
|
-
function ruleLead(sentence, keywordStart, contentStart) {
|
|
242
|
-
if (keywordStart < 0 || contentStart < 0 || contentStart > RULE_LEAD_CAP)
|
|
106
|
+
function extractFromPatterns(sentence, patterns, category) {
|
|
107
|
+
// A keyword inside brackets does not support a detached assertion; a later one outside them still does.
|
|
108
|
+
const keyed = patterns.some((pattern) => [...sentence.matchAll(new RegExp(pattern.source, `${pattern.flags}g`))]
|
|
109
|
+
.some((match) => !insideBrackets(sentence.slice(0, match.index))));
|
|
110
|
+
if (!keyed)
|
|
243
111
|
return null;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
else if ((ch === ')' || ch === ']' || ch === '}') && depth > 0)
|
|
249
|
-
depth--;
|
|
250
|
-
}
|
|
251
|
-
return depth > 0 ? null : sentence.slice(0, contentStart);
|
|
252
|
-
}
|
|
253
|
-
function extractFromPatterns(sentence, patterns, category, tag) {
|
|
254
|
-
for (const pat of patterns) {
|
|
255
|
-
// `d` gives per-group offsets; see the contentStart comment below.
|
|
256
|
-
const dpat = pat.flags.includes('d') ? pat : new RegExp(pat.source, pat.flags + 'd');
|
|
257
|
-
const match = dpat.exec(sentence);
|
|
258
|
-
if (match) {
|
|
259
|
-
let bounded;
|
|
260
|
-
if (pat === PREFERENCE_PATTERNS[0]) {
|
|
261
|
-
// PREFERENCE_PATTERNS[0] skips clause-bounding (see comment at its definition).
|
|
262
|
-
bounded = match[1] ?? match[0];
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// group1 = keyword + separator (verbatim, never clause-bounded);
|
|
266
|
-
// group2 = the content that follows it (clause-bounded below).
|
|
267
|
-
//
|
|
268
|
-
// The keyword is kept only when it carries SEMANTIC SIGN, a
|
|
269
|
-
// negation or modality ("never", "must not", "do not ever",
|
|
270
|
-
// "always"), because dropping those inverts the meaning. A LABEL keyword ("decision:", "rule:", "error:",
|
|
271
|
-
// "important:") carries no sign: it only names the category, which
|
|
272
|
-
// is already recorded in `category`/`tags`, so prefixing it onto the
|
|
273
|
-
// content is duplication that also breaks value-keyed matching
|
|
274
|
-
// (the rejected-value digest hashes the bare content).
|
|
275
|
-
// Discriminator: a label ends in its own colon, or is a "the X is"
|
|
276
|
-
// phrase.
|
|
277
|
-
const rawPrefix = match[1] ?? '';
|
|
278
|
-
// ONLY colon-terminated labels are dropped: dropping "the X is/was" leaves a residue starting
|
|
279
|
-
// with "to ", which isFragment rejects, so "The plan is to ship on Friday" would store nothing.
|
|
280
|
-
const isLabelPrefix = /:\s*$/.test(rawPrefix);
|
|
281
|
-
const keywordPrefix = isLabelPrefix ? '' : rawPrefix;
|
|
282
|
-
// Scan the UNTRUNCATED remainder, not match[2]: a quoted literal whose closer sits past the
|
|
283
|
-
// 500-char group cap would read as prose; boundToClause caps its OUTPUT, so this costs nothing.
|
|
284
|
-
// Group 2's REAL offset from the regex engine: DECISION_PATTERNS carry an uncaptured subject
|
|
285
|
-
// ("we ", "let's ") ahead of group 1, so `match.index + rawPrefix.length` lands inside the keyword.
|
|
286
|
-
const contentStart = match.indices?.[2]?.[0] ?? -1;
|
|
287
|
-
const afterKeyword = contentStart >= 0 ? sentence.slice(contentStart) : (match[2] ?? match[0]);
|
|
288
|
-
const keywordStart = match.indices?.[1]?.[0] ?? -1;
|
|
289
|
-
const lead = pat === RULE_PATTERNS[0] ? ruleLead(sentence, keywordStart, contentStart) : null;
|
|
290
|
-
bounded = lead !== null
|
|
291
|
-
? boundToClause(lead + afterKeyword, lead.length, keywordStart + 200)
|
|
292
|
-
: boundToClause(keywordPrefix + afterKeyword, keywordPrefix.length);
|
|
293
|
-
}
|
|
294
|
-
const content = cleanExtract(bounded);
|
|
295
|
-
if (content.length >= 8 && content.length <= 500) {
|
|
296
|
-
return { content, category, tags: [tag, 'captured'] };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
112
|
+
const content = sentence
|
|
113
|
+
.replace(/^(?:decision|plan|rule|important|critical|remember|error|bug|gotcha|watch out|careful|warning|caveat|trap|don't forget|easy to miss):\s*/i, '')
|
|
114
|
+
.replace(/[.!?\s]+$/, '').trim();
|
|
115
|
+
return content.length <= MAX_CHARS ? { content, category, tags: [category, 'captured'] } : null;
|
|
301
116
|
}
|
|
302
|
-
/** Extract spec items from bullet lists that follow spec-like headings. */
|
|
303
117
|
function extractSpecSections(text) {
|
|
304
|
-
const
|
|
305
|
-
const lines = text.split('\n');
|
|
118
|
+
const bullets = [];
|
|
306
119
|
let inSpecSection = false;
|
|
307
|
-
|
|
120
|
+
let open = false;
|
|
121
|
+
for (const line of text.split('\n')) {
|
|
308
122
|
const trimmed = line.trim();
|
|
309
|
-
|
|
310
|
-
if (SPEC_HEADING_PATTERNS.some((p) => p.test(trimmed))) {
|
|
123
|
+
if (SPEC_HEADING_PATTERNS.some(pattern => pattern.test(trimmed))) {
|
|
311
124
|
inSpecSection = true;
|
|
125
|
+
open = false;
|
|
312
126
|
continue;
|
|
313
127
|
}
|
|
314
|
-
|
|
315
|
-
if (/^#+\s/.test(trimmed) || /^[A-Z][a-z]+:$/.test(trimmed)) {
|
|
128
|
+
if (/^#+\s/.test(trimmed) || /^[A-Z][a-z]+:$/.test(trimmed))
|
|
316
129
|
inSpecSection = false;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
130
|
+
const bullet = trimmed.match(BULLET);
|
|
131
|
+
const wrapped = inSpecSection && open && !bullet && trimmed !== '' && (/^\s/.test(line) || continues(trimmed, bullets.at(-1)));
|
|
132
|
+
if (inSpecSection && bullet)
|
|
133
|
+
bullets.push(bullet[1].trim());
|
|
134
|
+
else if (wrapped)
|
|
135
|
+
bullets[bullets.length - 1] += ` ${trimmed}`;
|
|
136
|
+
open = inSpecSection && (bullet !== null || wrapped);
|
|
137
|
+
}
|
|
138
|
+
return bullets
|
|
139
|
+
.filter(content => content.length <= MAX_CHARS && !content.endsWith('?'))
|
|
140
|
+
.map(content => ({ content, category: 'spec', tags: ['spec', 'captured'] }));
|
|
141
|
+
}
|
|
142
|
+
function extractOne(text) {
|
|
143
|
+
const prose = proseOnly(text);
|
|
144
|
+
const items = extractSpecSections(prose);
|
|
145
|
+
for (const block of proseBlocks(prose)) {
|
|
146
|
+
for (const sentence of completeSentences(block)) {
|
|
147
|
+
// A question asks rather than states; far over the bound, skip before any pattern runs.
|
|
148
|
+
if (sentence.endsWith('?') || sentence.length > 2 * MAX_CHARS)
|
|
149
|
+
continue;
|
|
150
|
+
for (const [category, patterns] of CATEGORIES) {
|
|
151
|
+
const item = extractFromPatterns(sentence, patterns, category);
|
|
152
|
+
if (item) {
|
|
153
|
+
items.push(item);
|
|
154
|
+
break;
|
|
334
155
|
}
|
|
335
156
|
}
|
|
336
157
|
}
|
|
337
158
|
}
|
|
338
159
|
return items;
|
|
339
160
|
}
|
|
340
|
-
/**
|
|
341
|
-
|
|
342
|
-
*/
|
|
343
|
-
export function extractFromText(text) {
|
|
344
|
-
const items = [];
|
|
161
|
+
/** Captures complete supported statements, each text parsed alone so a fence or heading in one never reaches the next. */
|
|
162
|
+
export function extractFromTexts(texts) {
|
|
345
163
|
const seen = new Set();
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
if (seen.has(
|
|
349
|
-
return;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for (const item of extractSpecSections(text)) {
|
|
357
|
-
addIfNew(item);
|
|
358
|
-
}
|
|
359
|
-
// 2. Pattern-match on individual sentences
|
|
360
|
-
const sentences = splitSentences(text);
|
|
361
|
-
for (const sentence of sentences) {
|
|
362
|
-
// Try each category in priority order
|
|
363
|
-
const decision = extractFromPatterns(sentence, DECISION_PATTERNS, 'decision', 'decision');
|
|
364
|
-
if (decision) {
|
|
365
|
-
addIfNew(decision);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
const rule = extractFromPatterns(sentence, RULE_PATTERNS, 'rule', 'rule');
|
|
369
|
-
if (rule) {
|
|
370
|
-
addIfNew(rule);
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const error = extractFromPatterns(sentence, ERROR_PATTERNS, 'error', 'error');
|
|
374
|
-
if (error) {
|
|
375
|
-
addIfNew(error);
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
const preference = extractFromPatterns(sentence, PREFERENCE_PATTERNS, 'preference', 'preference');
|
|
379
|
-
if (preference) {
|
|
380
|
-
addIfNew(preference);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return items;
|
|
164
|
+
return texts.flatMap(extractOne).filter((item) => {
|
|
165
|
+
const key = duplicateKey(item.content);
|
|
166
|
+
if (seen.has(key) || !assessAutomaticMemory(item.content).accepted)
|
|
167
|
+
return false;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export function extractFromText(text) {
|
|
173
|
+
return extractFromTexts([text]);
|
|
385
174
|
}
|
|
386
175
|
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Context } from '../api/types.js';
|
|
2
|
+
import type { CallerProject } from '../prompt-hook.js';
|
|
3
|
+
import { type CaptureErrorOutcome, type RoutineRule } from './failure-reading.js';
|
|
4
|
+
type FailureSkip = Exclude<CaptureErrorOutcome, 'stored' | 'duplicate'>;
|
|
5
|
+
/** failureReport's reading of the payload, read on the caller's machine, plus the id its retries carry. */
|
|
6
|
+
export interface CallerFailureRequest {
|
|
7
|
+
readonly sessionId: string;
|
|
8
|
+
readonly project: CallerProject;
|
|
9
|
+
readonly tool: string | null;
|
|
10
|
+
readonly text: string | null;
|
|
11
|
+
readonly skip: FailureSkip | null;
|
|
12
|
+
readonly rule: RoutineRule | null;
|
|
13
|
+
readonly detailHash: string | null;
|
|
14
|
+
readonly requestId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CallerFailureResult {
|
|
17
|
+
readonly outcome: CaptureErrorOutcome;
|
|
18
|
+
}
|
|
19
|
+
export declare function captureFailureForCaller(ctx: Context, req: CallerFailureRequest): CallerFailureResult;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=failure-caller.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// PostToolUseFailure for a caller on another machine: the lesson and its log row carry the caller's owner and project, and a retried request id gets the first answer.
|
|
2
|
+
import { BadRequestError } from '../api-errors.js';
|
|
3
|
+
import { storeLesson } from '../capture-error.js';
|
|
4
|
+
import { recordFailure, requestOutcome, settleFailureOutcome } from '../failure-log.js';
|
|
5
|
+
import { errorMessage, log } from '../log.js';
|
|
6
|
+
import { scrubForSharing } from '../share-scrub.js';
|
|
7
|
+
import { truncateCodePointSafe } from '../transcript-tail.js';
|
|
8
|
+
import { assertRequestId, bindCaller, withCallerDb } from './caller-session.js';
|
|
9
|
+
import { FAILURE_TEXT_MAX_CHARS, failureHash } from './failure-reading.js';
|
|
10
|
+
const SKIPS = { 'skipped-interrupt': true, 'skipped-routine': true, 'skipped-invalid': true };
|
|
11
|
+
const ROUTINE_RULES = { declined: true, 'os-permission': true, 'no-match': true, 'search-tool': true, 'quiet-exit': true };
|
|
12
|
+
/** The shapes lessonFromFailure gives: text for a lesson or a routine skip, none for an interrupt or an unreadable payload, a rule only on a routine skip. */
|
|
13
|
+
function checkedFailure(req) {
|
|
14
|
+
assertRequestId(req.requestId);
|
|
15
|
+
if (req.detailHash !== null && !/^[0-9a-f]{16}$/.test(req.detailHash))
|
|
16
|
+
throw new BadRequestError('detail hash: 16 lowercase hex characters or null');
|
|
17
|
+
const { skip, text, rule } = req;
|
|
18
|
+
if (skip !== null && !Object.hasOwn(SKIPS, skip))
|
|
19
|
+
throw new BadRequestError('skip: skipped-interrupt, skipped-routine, skipped-invalid or null');
|
|
20
|
+
if ((skip === 'skipped-routine') !== (rule !== null) || (rule !== null && !Object.hasOwn(ROUTINE_RULES, rule))) {
|
|
21
|
+
throw new BadRequestError('rule: a routine rule on skipped-routine, else null');
|
|
22
|
+
}
|
|
23
|
+
if (text === null || text.trim() === '') {
|
|
24
|
+
if (skip === null || skip === 'skipped-routine')
|
|
25
|
+
throw new BadRequestError('text: required for a lesson or a routine skip');
|
|
26
|
+
return { skip, text: null };
|
|
27
|
+
}
|
|
28
|
+
if (skip === 'skipped-interrupt' || skip === 'skipped-invalid')
|
|
29
|
+
throw new BadRequestError('text: null for an interrupt or an unreadable failure');
|
|
30
|
+
if (text.length > FAILURE_TEXT_MAX_CHARS)
|
|
31
|
+
throw new BadRequestError(`text: at most ${FAILURE_TEXT_MAX_CHARS} characters`);
|
|
32
|
+
// Scrubbed again, as the caller's scrub is not trusted, and cut after it, as a mask can be longer than what it hides.
|
|
33
|
+
return { skip, text: truncateCodePointSafe(scrubForSharing(text), FAILURE_TEXT_MAX_CHARS) };
|
|
34
|
+
}
|
|
35
|
+
export function captureFailureForCaller(ctx, req) {
|
|
36
|
+
const failure = checkedFailure(req);
|
|
37
|
+
const key = bindCaller(ctx, req.sessionId, req.project);
|
|
38
|
+
const earlier = withCallerDb(ctx, (db) => requestOutcome(db, ctx.tenantId, req.requestId, req.sessionId));
|
|
39
|
+
// A retry after a lost reply gets the first answer and writes nothing; one whose store failed tries the store again.
|
|
40
|
+
if (earlier !== null && earlier !== 'store-failed')
|
|
41
|
+
return { outcome: earlier };
|
|
42
|
+
let logged = 'store-failed';
|
|
43
|
+
try {
|
|
44
|
+
const caller = { actor: key.owner, originProject: req.project.name, origins: key.project };
|
|
45
|
+
const outcome = failure.skip === null ? storeLesson(ctx.hippoRoot, ctx.tenantId, failure.text, caller) : failure.skip;
|
|
46
|
+
logged = outcome;
|
|
47
|
+
return { outcome };
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
// The hash is of the text as stored, so a repeat counts against the lesson it repeats.
|
|
51
|
+
logOutcome(ctx, { ...req, text: failure.text }, key, logged, earlier !== null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Runs after the outcome in its own try, so a log error never hides the store's error or its outcome. */
|
|
55
|
+
function logOutcome(ctx, req, key, outcome, retried) {
|
|
56
|
+
try {
|
|
57
|
+
withCallerDb(ctx, (db) => {
|
|
58
|
+
// The request id allows one row, so a retry rewrites the first try's outcome.
|
|
59
|
+
if (retried) {
|
|
60
|
+
settleFailureOutcome(db, ctx.tenantId, req.requestId, outcome);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
recordFailure(db, {
|
|
64
|
+
tenantId: ctx.tenantId, sessionId: req.sessionId, tool: req.tool, outcome, rule: req.rule,
|
|
65
|
+
sigHash: req.text?.trim() ? failureHash(req.text) : null, detailHash: req.detailHash,
|
|
66
|
+
ownerSubject: key.owner, originProject: req.project.name, requestId: req.requestId,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
log.warn(`capture-error: failure not logged: ${errorMessage(err)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=failure-caller.js.map
|