opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.8
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/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Lifecycle — state machine for context items.
|
|
3
|
+
*
|
|
4
|
+
* States: CREATED, ACTIVE, COMPRESSED, STALE, INVALIDATED, ARCHIVED.
|
|
5
|
+
*
|
|
6
|
+
* Each ContextItem progresses through these states as the working
|
|
7
|
+
* session evolves. The lifecycle module provides:
|
|
8
|
+
* - state constants and transition table
|
|
9
|
+
* - transition() with validation against the table
|
|
10
|
+
* - validity checks (isUsable, isTerminal)
|
|
11
|
+
* - time-based staleness helper
|
|
12
|
+
*
|
|
13
|
+
* Designed to operate on top of ContextItem produced by
|
|
14
|
+
* context-normalization.js without coupling to specific source payloads.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {"CREATED"|"ACTIVE"|"COMPRESSED"|"STALE"|"INVALIDATED"|"ARCHIVED"} LifecycleState
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {Record<LifecycleState, LifecycleState>} */
|
|
22
|
+
export const LIFECYCLE_STATES = Object.freeze({
|
|
23
|
+
CREATED: "CREATED",
|
|
24
|
+
ACTIVE: "ACTIVE",
|
|
25
|
+
COMPRESSED: "COMPRESSED",
|
|
26
|
+
STALE: "STALE",
|
|
27
|
+
INVALIDATED: "INVALIDATED",
|
|
28
|
+
ARCHIVED: "ARCHIVED",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Allowed transitions. Terminal states (INVALIDATED, ARCHIVED) have no
|
|
33
|
+
* outgoing edges — once archived/invalidated, the item is read-only.
|
|
34
|
+
* @type {Record<LifecycleState, LifecycleState[]>}
|
|
35
|
+
*/
|
|
36
|
+
export const TRANSITIONS = Object.freeze({
|
|
37
|
+
CREATED: ["ACTIVE", "STALE", "INVALIDATED", "ARCHIVED"],
|
|
38
|
+
ACTIVE: ["COMPRESSED", "STALE", "INVALIDATED", "ARCHIVED"],
|
|
39
|
+
COMPRESSED: ["ACTIVE", "STALE", "INVALIDATED", "ARCHIVED"],
|
|
40
|
+
STALE: ["ACTIVE", "INVALIDATED", "ARCHIVED", "COMPRESSED"],
|
|
41
|
+
INVALIDATED: [],
|
|
42
|
+
ARCHIVED: [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** @type {LifecycleState[]} */
|
|
46
|
+
export const TERMINAL_STATES = Object.freeze(["INVALIDATED", "ARCHIVED"]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate that a state value is a known lifecycle state.
|
|
50
|
+
* @param {unknown} state
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function isLifecycleState(state) {
|
|
54
|
+
return typeof state === "string" && state in LIFECYCLE_STATES;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check whether `from -> to` is a permitted transition.
|
|
59
|
+
* @param {LifecycleState} from
|
|
60
|
+
* @param {LifecycleState} to
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function canTransition(from, to) {
|
|
64
|
+
if (!isLifecycleState(from) || !isLifecycleState(to)) return false;
|
|
65
|
+
return TRANSITIONS[from].includes(to);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Transition a context item from its current state to `next`.
|
|
70
|
+
* Returns a NEW item with updated lifecycle metadata; the original is
|
|
71
|
+
* not mutated. Throws if the transition is not allowed.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} item - ContextItem with `lifecycle` field
|
|
74
|
+
* @param {LifecycleState} next
|
|
75
|
+
* @param {Object} [meta] - Optional metadata to record on the transition
|
|
76
|
+
* (e.g. { reason, ts, actor })
|
|
77
|
+
* @returns {Object} Updated item
|
|
78
|
+
*/
|
|
79
|
+
export function transition(item, next, meta = {}) {
|
|
80
|
+
if (!item || typeof item !== "object") {
|
|
81
|
+
throw new TypeError("item must be a non-null object");
|
|
82
|
+
}
|
|
83
|
+
if (!isLifecycleState(next)) {
|
|
84
|
+
throw new TypeError(`Unknown lifecycle state: ${String(next)}`);
|
|
85
|
+
}
|
|
86
|
+
const current = item.lifecycle || LIFECYCLE_STATES.CREATED;
|
|
87
|
+
if (!isLifecycleState(current)) {
|
|
88
|
+
throw new TypeError(`Item has invalid lifecycle: ${String(current)}`);
|
|
89
|
+
}
|
|
90
|
+
if (!canTransition(current, next)) {
|
|
91
|
+
throw new Error(`Illegal transition: ${current} -> ${next}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const history = Array.isArray(item.lifecycleHistory)
|
|
95
|
+
? [...item.lifecycleHistory, { from: current, to: next, ts: Date.now(), ...meta }]
|
|
96
|
+
: [{ from: current, to: next, ts: Date.now(), ...meta }];
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...item,
|
|
100
|
+
lifecycle: next,
|
|
101
|
+
lifecycleHistory: history,
|
|
102
|
+
updatedAt: Date.now(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Decide whether `item` is in a state usable for assembly/retrieval.
|
|
108
|
+
* Items in COMPRESSED, STALE, INVALIDATED or ARCHIVED are excluded.
|
|
109
|
+
* @param {Object} item
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
export function isUsable(item) {
|
|
113
|
+
if (!item || typeof item !== "object") return false;
|
|
114
|
+
const state = item.lifecycle || LIFECYCLE_STATES.CREATED;
|
|
115
|
+
return state === LIFECYCLE_STATES.CREATED || state === LIFECYCLE_STATES.ACTIVE;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check whether the state is terminal (no further transitions possible).
|
|
120
|
+
* @param {LifecycleState} state
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
export function isTerminal(state) {
|
|
124
|
+
return TERMINAL_STATES.includes(state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Default staleness threshold per state, in milliseconds. Items older than
|
|
129
|
+
* (now - timestamp) > threshold are candidates for transition to STALE.
|
|
130
|
+
* @type {Record<LifecycleState, number>}
|
|
131
|
+
*/
|
|
132
|
+
export const STALE_THRESHOLD_MS = Object.freeze({
|
|
133
|
+
CREATED: 60 * 60 * 1000, // 1h
|
|
134
|
+
ACTIVE: 24 * 60 * 60 * 1000, // 24h
|
|
135
|
+
COMPRESSED: 7 * 24 * 60 * 60 * 1000, // 7d
|
|
136
|
+
STALE: Infinity,
|
|
137
|
+
INVALIDATED: Infinity,
|
|
138
|
+
ARCHIVED: Infinity,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compute the recommended next state based on age. Returns the current
|
|
143
|
+
* state if it is still fresh, or STALE if it has aged past threshold.
|
|
144
|
+
* @param {Object} item
|
|
145
|
+
* @param {number} [now]
|
|
146
|
+
* @returns {LifecycleState}
|
|
147
|
+
*/
|
|
148
|
+
export function stalenessCheck(item, now = Date.now()) {
|
|
149
|
+
if (!item || typeof item !== "object") {
|
|
150
|
+
throw new TypeError("item must be a non-null object");
|
|
151
|
+
}
|
|
152
|
+
const state = item.lifecycle || LIFECYCLE_STATES.CREATED;
|
|
153
|
+
const ts = typeof item.timestamp === "number" ? item.timestamp : now;
|
|
154
|
+
const threshold = STALE_THRESHOLD_MS[state];
|
|
155
|
+
if (threshold === Infinity) return state;
|
|
156
|
+
return now - ts > threshold ? LIFECYCLE_STATES.STALE : state;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default {
|
|
160
|
+
LIFECYCLE_STATES,
|
|
161
|
+
TRANSITIONS,
|
|
162
|
+
TERMINAL_STATES,
|
|
163
|
+
STALE_THRESHOLD_MS,
|
|
164
|
+
isLifecycleState,
|
|
165
|
+
canTransition,
|
|
166
|
+
transition,
|
|
167
|
+
isUsable,
|
|
168
|
+
isTerminal,
|
|
169
|
+
stalenessCheck,
|
|
170
|
+
};
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Loop Prevention — Detect repetitive query/source/decision loops.
|
|
3
|
+
*
|
|
4
|
+
* Change 80: Loop detection based on identical query/sources/decision sequences.
|
|
5
|
+
*
|
|
6
|
+
* When an agent repeatedly asks for the same context, makes the same
|
|
7
|
+
* decisions, or follows identical reasoning chains, this module detects
|
|
8
|
+
* the loop and signals that intervention is needed.
|
|
9
|
+
*
|
|
10
|
+
* Detection strategies:
|
|
11
|
+
* 1. Exact query repetition
|
|
12
|
+
* 2. Source set repetition (same sources used N times)
|
|
13
|
+
* 3. Decision sequence repetition (same decisions in same order)
|
|
14
|
+
* 4. Action hash cycle detection
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} LoopEvent
|
|
19
|
+
* @property {string} id - Event identifier
|
|
20
|
+
* @property {string} type - "query" | "source" | "decision" | "action"
|
|
21
|
+
* @property {string} fingerprint - Hash/key for deduplication
|
|
22
|
+
* @property {number} timestamp - Unix ms
|
|
23
|
+
* @property {Object} [data] - Additional context
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} LoopDetection
|
|
28
|
+
* @property {boolean} loopDetected
|
|
29
|
+
* @property {string} type - Type of loop detected
|
|
30
|
+
* @property {number} occurrences - How many times the pattern repeated
|
|
31
|
+
* @property {number} windowStart - First occurrence timestamp
|
|
32
|
+
* @property {number} windowEnd - Last occurrence timestamp
|
|
33
|
+
* @property {string[]} items - The repeated items
|
|
34
|
+
* @property {number} confidence - 0-1 confidence of loop
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} LoopConfig
|
|
39
|
+
* @property {number} [maxEvents=1000] - Max events to track
|
|
40
|
+
* @property {number} [minOccurrences=3] - Min repetitions to flag loop
|
|
41
|
+
* @property {number} [windowMs=300000] - Time window for loop detection (5min default)
|
|
42
|
+
* @property {number} [maxUniqueItems=100] - Max unique fingerprints to track
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { hashStrategy, sameStrategy } from "../execution/strategy-identity.js";
|
|
46
|
+
|
|
47
|
+
const DEFAULT_CONFIG = {
|
|
48
|
+
maxEvents: 1000,
|
|
49
|
+
minOccurrences: 3,
|
|
50
|
+
windowMs: 5 * 60 * 1000, // 5 minutes
|
|
51
|
+
maxUniqueItems: 100,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Simple deterministic hash for strings.
|
|
56
|
+
* @param {string} str
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function simpleHash(str) {
|
|
60
|
+
let hash = 0;
|
|
61
|
+
for (let i = 0; i < str.length; i++) {
|
|
62
|
+
const char = str.charCodeAt(i);
|
|
63
|
+
hash = (hash << 5) - hash + char;
|
|
64
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
65
|
+
}
|
|
66
|
+
return Math.abs(hash).toString(36);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a fingerprint for a query.
|
|
71
|
+
* @param {Object} query
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function queryFingerprint(query) {
|
|
75
|
+
if (typeof query === "string") return simpleHash(query);
|
|
76
|
+
if (query && query.text) return simpleHash(query.text);
|
|
77
|
+
if (query && query.q) return simpleHash(query.q);
|
|
78
|
+
return simpleHash(JSON.stringify(query || ""));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class ContextLoopDetector {
|
|
82
|
+
/**
|
|
83
|
+
* @param {LoopConfig} [config={}]
|
|
84
|
+
*/
|
|
85
|
+
constructor(config = {}) {
|
|
86
|
+
this._config = { ...DEFAULT_CONFIG, ...config };
|
|
87
|
+
/** @type {LoopEvent[]} */
|
|
88
|
+
this._events = [];
|
|
89
|
+
/** @type {Map<string, number[]>} */
|
|
90
|
+
this._occurrenceIndex = new Map(); // fingerprint -> timestamps
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record a query/action/decision event.
|
|
95
|
+
* @param {"query"|"source"|"decision"|"action"} type
|
|
96
|
+
* @param {Object|string} item - The item or query string
|
|
97
|
+
* @param {Object} [data] - Additional data
|
|
98
|
+
* @returns {LoopEvent}
|
|
99
|
+
*/
|
|
100
|
+
record(type, item, data = {}) {
|
|
101
|
+
if (!type || !item) {
|
|
102
|
+
throw new TypeError("type and item are required");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fingerprint = this._fingerprint(type, item);
|
|
106
|
+
const timestamp = Date.now();
|
|
107
|
+
|
|
108
|
+
const event = {
|
|
109
|
+
id: `loop_${simpleHash(fingerprint + timestamp)}`,
|
|
110
|
+
type,
|
|
111
|
+
fingerprint,
|
|
112
|
+
timestamp,
|
|
113
|
+
data,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
this._events.push(event);
|
|
117
|
+
|
|
118
|
+
// Track occurrences
|
|
119
|
+
if (!this._occurrenceIndex.has(fingerprint)) {
|
|
120
|
+
this._occurrenceIndex.set(fingerprint, []);
|
|
121
|
+
}
|
|
122
|
+
const timestamps = this._occurrenceIndex.get(fingerprint);
|
|
123
|
+
timestamps.push(timestamp);
|
|
124
|
+
|
|
125
|
+
// Cleanup: prune old events beyond max
|
|
126
|
+
if (this._events.length > this._config.maxEvents) {
|
|
127
|
+
this._events = this._events.slice(-this._config.maxEvents);
|
|
128
|
+
}
|
|
129
|
+
if (this._occurrenceIndex.size > this._config.maxUniqueItems) {
|
|
130
|
+
// Remove oldest entries
|
|
131
|
+
const entries = [...this._occurrenceIndex.entries()].sort(
|
|
132
|
+
(a, b) => Math.min(...a[1]) - Math.min(...b[1])
|
|
133
|
+
);
|
|
134
|
+
for (let i = 0; i < Math.floor(this._config.maxUniqueItems / 2); i++) {
|
|
135
|
+
this._occurrenceIndex.delete(entries[i][0]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return event;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate fingerprint based on event type.
|
|
144
|
+
*/
|
|
145
|
+
_fingerprint(type, item) {
|
|
146
|
+
if (type === "query") return queryFingerprint(item);
|
|
147
|
+
if (type === "source") {
|
|
148
|
+
const src = typeof item === "string" ? item : item.id || item.name || JSON.stringify(item);
|
|
149
|
+
return `src:${simpleHash(src)}`;
|
|
150
|
+
}
|
|
151
|
+
if (type === "decision") {
|
|
152
|
+
return `dec:${simpleHash(JSON.stringify(item))}`;
|
|
153
|
+
}
|
|
154
|
+
if (type === "action") {
|
|
155
|
+
const act = typeof item === "string" ? item : item.type || item.name || JSON.stringify(item);
|
|
156
|
+
return `act:${simpleHash(act)}`;
|
|
157
|
+
}
|
|
158
|
+
return simpleHash(JSON.stringify(item));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if a fingerprint has been seen within the time window.
|
|
163
|
+
* @param {string} fingerprint
|
|
164
|
+
* @returns {LoopDetection | null}
|
|
165
|
+
*/
|
|
166
|
+
check(fingerprint) {
|
|
167
|
+
const timestamps = this._occurrenceIndex.get(fingerprint);
|
|
168
|
+
if (!timestamps || timestamps.length < this._config.minOccurrences) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const windowStart = now - this._config.windowMs;
|
|
174
|
+
const recent = timestamps.filter((t) => t >= windowStart);
|
|
175
|
+
|
|
176
|
+
if (recent.length < this._config.minOccurrences) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
loopDetected: true,
|
|
182
|
+
type: "repetition",
|
|
183
|
+
occurrences: recent.length,
|
|
184
|
+
windowStart: recent[0],
|
|
185
|
+
windowEnd: recent[recent.length - 1],
|
|
186
|
+
items: [fingerprint],
|
|
187
|
+
confidence: Math.min(1, recent.length / (this._config.minOccurrences + 2)),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check for loops across all tracked fingerprints.
|
|
193
|
+
* @returns {LoopDetection[]}
|
|
194
|
+
*/
|
|
195
|
+
checkAll() {
|
|
196
|
+
const loops = [];
|
|
197
|
+
for (const [fingerprint] of this._occurrenceIndex) {
|
|
198
|
+
const result = this.check(fingerprint);
|
|
199
|
+
if (result) {
|
|
200
|
+
loops.push(result);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return loops.sort((a, b) => b.occurrences - a.occurrences);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Record a query and immediately check for loops.
|
|
208
|
+
* @param {Object|string} query
|
|
209
|
+
* @param {Object} [data]
|
|
210
|
+
* @returns {LoopDetection | null}
|
|
211
|
+
*/
|
|
212
|
+
recordQuery(query, data = {}) {
|
|
213
|
+
this.record("query", query, data);
|
|
214
|
+
return this.check(queryFingerprint(query));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Record a decision sequence and check if the same sequence repeats.
|
|
219
|
+
* @param {string[]} decisions - Ordered list of decision IDs
|
|
220
|
+
* @returns {LoopDetection | null}
|
|
221
|
+
*/
|
|
222
|
+
recordDecisionSequence(decisions) {
|
|
223
|
+
if (!Array.isArray(decisions)) {
|
|
224
|
+
throw new TypeError("decisions must be an array");
|
|
225
|
+
}
|
|
226
|
+
const fingerprint = `seq:${simpleHash(decisions.join("|"))}`;
|
|
227
|
+
this.record("decision", decisions);
|
|
228
|
+
return this.check(fingerprint);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get statistics about recorded events.
|
|
233
|
+
* @returns {Object}
|
|
234
|
+
*/
|
|
235
|
+
stats() {
|
|
236
|
+
let mostCommon = null;
|
|
237
|
+
let maxCount = 0;
|
|
238
|
+
for (const [fp, timestamps] of this._occurrenceIndex) {
|
|
239
|
+
if (timestamps.length > maxCount) {
|
|
240
|
+
maxCount = timestamps.length;
|
|
241
|
+
mostCommon = fp;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
totalEvents: this._events.length,
|
|
246
|
+
uniqueFingerprints: this._occurrenceIndex.size,
|
|
247
|
+
mostRepeated: mostCommon ? { fingerprint: mostCommon, count: maxCount } : null,
|
|
248
|
+
config: this._config,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Clear all recorded data.
|
|
254
|
+
*/
|
|
255
|
+
reset() {
|
|
256
|
+
this._events = [];
|
|
257
|
+
this._occurrenceIndex.clear();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Factory function for creating a loop detector (convenience).
|
|
263
|
+
* @param {LoopConfig} [config={}]
|
|
264
|
+
* @returns {ContextLoopDetector}
|
|
265
|
+
*/
|
|
266
|
+
export function createLoopDetector(config = {}) {
|
|
267
|
+
return new ContextLoopDetector(config);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Quick one-shot loop check: given an array of recent items (strings),
|
|
272
|
+
* detect if any item appears too frequently.
|
|
273
|
+
* @param {string[]} items
|
|
274
|
+
* @param {number} [threshold=3]
|
|
275
|
+
* @returns {string[]} Items that repeat too often
|
|
276
|
+
*/
|
|
277
|
+
export function quickLoopCheck(items, threshold = 3) {
|
|
278
|
+
const counts = new Map();
|
|
279
|
+
for (const item of items) {
|
|
280
|
+
const key = typeof item === "string" ? item : JSON.stringify(item);
|
|
281
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
282
|
+
}
|
|
283
|
+
const loops = [];
|
|
284
|
+
for (const [item, count] of counts) {
|
|
285
|
+
if (count >= threshold) {
|
|
286
|
+
loops.push(item);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return loops;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Strategy repetition check: given a sequence of approvedStrategy snapshots
|
|
294
|
+
* (in chronological order), detect whether the agent is re-running the SAME
|
|
295
|
+
* strategy N times despite different surface outcomes. This catches cases
|
|
296
|
+
* where the user / system keeps approving identical strategies without progress.
|
|
297
|
+
*
|
|
298
|
+
* Uses canonical hashing (strategy-identity.hashStrategy) so volatile fields
|
|
299
|
+
* (timestamps, runIds, instanceIds) do NOT affect equality.
|
|
300
|
+
*
|
|
301
|
+
* @param {Object[]} strategySnapshots - Array of approvedStrategy objects
|
|
302
|
+
* @param {number} [threshold=3] - Minimum repetitions to flag a strategy loop
|
|
303
|
+
* @returns {{ loopDetected: boolean, hash?: string, occurrences?: number, strategy?: Object }}
|
|
304
|
+
*/
|
|
305
|
+
export function detectStrategyRepetition(strategySnapshots, threshold = 3) {
|
|
306
|
+
if (!Array.isArray(strategySnapshots) || strategySnapshots.length < threshold) {
|
|
307
|
+
return { loopDetected: false };
|
|
308
|
+
}
|
|
309
|
+
const counts = new Map();
|
|
310
|
+
for (const snapshot of strategySnapshots) {
|
|
311
|
+
const hash = hashStrategy(snapshot);
|
|
312
|
+
if (hash === null) continue;
|
|
313
|
+
counts.set(hash, (counts.get(hash) || 0) + 1);
|
|
314
|
+
}
|
|
315
|
+
let topHash = null;
|
|
316
|
+
let topCount = 0;
|
|
317
|
+
let topStrategy = null;
|
|
318
|
+
for (const [hash, count] of counts) {
|
|
319
|
+
if (count > topCount) {
|
|
320
|
+
topCount = count;
|
|
321
|
+
topHash = hash;
|
|
322
|
+
topStrategy = strategySnapshots.find((s) => hashStrategy(s) === hash) || null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (topCount >= threshold) {
|
|
326
|
+
return {
|
|
327
|
+
loopDetected: true,
|
|
328
|
+
hash: topHash,
|
|
329
|
+
occurrences: topCount,
|
|
330
|
+
strategy: topStrategy,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
return { loopDetected: false };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Compare two approvedStrategy objects for semantic identity, ignoring
|
|
338
|
+
* volatile fields. Thin wrapper around strategy-identity.sameStrategy.
|
|
339
|
+
*
|
|
340
|
+
* @param {Object} a
|
|
341
|
+
* @param {Object} b
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
export function sameStrategyIdentity(a, b) {
|
|
345
|
+
return sameStrategy(a, b);
|
|
346
|
+
}
|
|
347
|
+
export { ContextLoopDetector };
|