screenhand 0.1.1 → 0.3.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 +193 -109
- package/bin/darwin-arm64/macos-bridge +0 -0
- package/dist/mcp-desktop.js +5876 -0
- package/dist/scripts/codex-monitor-daemon.js +335 -0
- package/dist/scripts/export-help-center.js +112 -0
- package/dist/scripts/marketing-loop.js +117 -0
- package/dist/scripts/observer-daemon.js +288 -0
- package/dist/scripts/orchestrator-daemon.js +399 -0
- package/dist/scripts/supervisor-daemon.js +272 -0
- package/dist/scripts/threads-campaign.js +208 -0
- package/dist/scripts/worker-daemon.js +228 -0
- package/dist/src/agent/cli.js +82 -0
- package/dist/src/agent/loop.js +274 -0
- package/dist/src/community/fetcher.js +109 -0
- package/dist/src/community/index.js +6 -0
- package/dist/src/community/publisher.js +191 -0
- package/dist/src/community/remote-api.js +121 -0
- package/dist/src/community/types.js +3 -0
- package/dist/src/community/validator.js +95 -0
- package/{src/config.ts → dist/src/config.js} +5 -10
- package/dist/src/context-tracker.js +489 -0
- package/{src/index.ts → dist/src/index.js} +32 -52
- package/dist/src/ingestion/coverage-auditor.js +233 -0
- package/dist/src/ingestion/doc-parser.js +164 -0
- package/dist/src/ingestion/index.js +8 -0
- package/dist/src/ingestion/menu-scanner.js +152 -0
- package/dist/src/ingestion/reference-merger.js +186 -0
- package/dist/src/ingestion/shortcut-extractor.js +180 -0
- package/dist/src/ingestion/tutorial-extractor.js +170 -0
- package/dist/src/ingestion/types.js +3 -0
- package/dist/src/jobs/manager.js +305 -0
- package/dist/src/jobs/runner.js +806 -0
- package/dist/src/jobs/store.js +102 -0
- package/dist/src/jobs/types.js +30 -0
- package/dist/src/jobs/worker.js +97 -0
- package/dist/src/learning/engine.js +356 -0
- package/dist/src/learning/index.js +9 -0
- package/dist/src/learning/locator-policy.js +120 -0
- package/dist/src/learning/pattern-policy.js +89 -0
- package/dist/src/learning/recovery-policy.js +116 -0
- package/dist/src/learning/sensor-policy.js +115 -0
- package/dist/src/learning/timing-model.js +204 -0
- package/dist/src/learning/topology-policy.js +90 -0
- package/dist/src/learning/types.js +9 -0
- package/dist/src/logging/timeline-logger.js +48 -0
- package/dist/src/mcp/mcp-stdio-server.js +464 -0
- package/dist/src/mcp/server.js +363 -0
- package/dist/src/mcp-entry.js +60 -0
- package/dist/src/memory/playbook-seeds.js +200 -0
- package/dist/src/memory/recall.js +222 -0
- package/dist/src/memory/research.js +104 -0
- package/dist/src/memory/seeds.js +101 -0
- package/dist/src/memory/service.js +446 -0
- package/dist/src/memory/session.js +169 -0
- package/dist/src/memory/store.js +451 -0
- package/{src/runtime/locator-cache.ts → dist/src/memory/types.js} +1 -17
- package/dist/src/monitor/codex-monitor.js +382 -0
- package/dist/src/monitor/task-queue.js +97 -0
- package/dist/src/monitor/types.js +62 -0
- package/dist/src/native/bridge-client.js +412 -0
- package/{src/native/macos-bridge-client.ts → dist/src/native/macos-bridge-client.js} +0 -1
- package/dist/src/observer/state.js +199 -0
- package/dist/src/observer/types.js +43 -0
- package/dist/src/orchestrator/state.js +68 -0
- package/dist/src/orchestrator/types.js +22 -0
- package/dist/src/perception/ax-source.js +162 -0
- package/dist/src/perception/cdp-source.js +162 -0
- package/dist/src/perception/coordinator.js +771 -0
- package/dist/src/perception/frame-differ.js +287 -0
- package/dist/src/perception/index.js +22 -0
- package/dist/src/perception/manager.js +199 -0
- package/dist/src/perception/types.js +47 -0
- package/dist/src/perception/vision-source.js +399 -0
- package/dist/src/planner/deterministic.js +298 -0
- package/dist/src/planner/executor.js +870 -0
- package/dist/src/planner/goal-store.js +92 -0
- package/dist/src/planner/index.js +21 -0
- package/dist/src/planner/planner.js +520 -0
- package/dist/src/planner/tool-registry.js +71 -0
- package/dist/src/planner/types.js +22 -0
- package/dist/src/platform/explorer.js +213 -0
- package/dist/src/platform/help-center-markdown.js +527 -0
- package/dist/src/platform/learner.js +257 -0
- package/dist/src/playbook/engine.js +486 -0
- package/dist/src/playbook/index.js +20 -0
- package/dist/src/playbook/mcp-recorder.js +204 -0
- package/dist/src/playbook/recorder.js +536 -0
- package/dist/src/playbook/runner.js +408 -0
- package/dist/src/playbook/store.js +312 -0
- package/dist/src/playbook/types.js +17 -0
- package/dist/src/recovery/detectors.js +156 -0
- package/dist/src/recovery/engine.js +327 -0
- package/dist/src/recovery/index.js +20 -0
- package/dist/src/recovery/strategies.js +274 -0
- package/dist/src/recovery/types.js +20 -0
- package/dist/src/runtime/accessibility-adapter.js +430 -0
- package/dist/src/runtime/app-adapter.js +64 -0
- package/dist/src/runtime/applescript-adapter.js +305 -0
- package/dist/src/runtime/ax-role-map.js +96 -0
- package/dist/src/runtime/browser-adapter.js +52 -0
- package/dist/src/runtime/cdp-chrome-adapter.js +521 -0
- package/dist/src/runtime/composite-adapter.js +221 -0
- package/dist/src/runtime/execution-contract.js +159 -0
- package/dist/src/runtime/executor.js +286 -0
- package/dist/src/runtime/locator-cache.js +50 -0
- package/dist/src/runtime/planning-loop.js +63 -0
- package/dist/src/runtime/service.js +432 -0
- package/dist/src/runtime/session-manager.js +63 -0
- package/dist/src/runtime/state-observer.js +121 -0
- package/dist/src/runtime/vision-adapter.js +225 -0
- package/dist/src/state/app-map-types.js +72 -0
- package/dist/src/state/app-map.js +1974 -0
- package/dist/src/state/entity-tracker.js +108 -0
- package/dist/src/state/fusion.js +96 -0
- package/dist/src/state/index.js +21 -0
- package/dist/src/state/ladder-generator.js +236 -0
- package/dist/src/state/persistence.js +156 -0
- package/dist/src/state/types.js +17 -0
- package/dist/src/state/world-model.js +1456 -0
- package/dist/src/supervisor/locks.js +186 -0
- package/dist/src/supervisor/supervisor.js +403 -0
- package/dist/src/supervisor/types.js +30 -0
- package/dist/src/test-mcp-protocol.js +154 -0
- package/dist/src/types.js +17 -0
- package/dist/src/util/atomic-write.js +133 -0
- package/dist/src/util/sanitize.js +146 -0
- package/dist-app-maps/com.figma.Desktop.json +959 -0
- package/dist-app-maps/com.hnc.Discord.json +1146 -0
- package/dist-app-maps/notion.id.json +2831 -0
- package/dist-playbooks/canva-screenhand-carousel.json +445 -0
- package/dist-playbooks/codex-desktop.json +76 -0
- package/dist-playbooks/competitor-research-stack.json +122 -0
- package/dist-playbooks/davinci-color-grade.json +153 -0
- package/dist-playbooks/davinci-edit-timeline.json +162 -0
- package/dist-playbooks/davinci-render.json +114 -0
- package/dist-playbooks/devto.json +52 -0
- package/dist-playbooks/discord.json +41 -0
- package/dist-playbooks/google-flow-create-project.json +59 -0
- package/dist-playbooks/google-flow-edit-image.json +90 -0
- package/dist-playbooks/google-flow-edit-video.json +90 -0
- package/dist-playbooks/google-flow-generate-image.json +68 -0
- package/dist-playbooks/google-flow-generate-video.json +191 -0
- package/dist-playbooks/google-flow-open-project.json +48 -0
- package/dist-playbooks/google-flow-open-scenebuilder.json +64 -0
- package/dist-playbooks/google-flow-search-assets.json +64 -0
- package/dist-playbooks/instagram.json +57 -0
- package/dist-playbooks/linkedin.json +52 -0
- package/dist-playbooks/n8n.json +43 -0
- package/dist-playbooks/reddit.json +52 -0
- package/dist-playbooks/threads.json +59 -0
- package/dist-playbooks/x-twitter.json +59 -0
- package/dist-playbooks/youtube.json +59 -0
- package/dist-references/canva.json +646 -0
- package/dist-references/codex-desktop.json +305 -0
- package/dist-references/davinci-resolve-keyboard.json +594 -0
- package/dist-references/davinci-resolve-menu-map.json +1139 -0
- package/dist-references/davinci-resolve-menus-batch1.json +116 -0
- package/dist-references/davinci-resolve-menus-batch2.json +372 -0
- package/dist-references/davinci-resolve-menus-batch3.json +330 -0
- package/dist-references/davinci-resolve-menus-batch4.json +297 -0
- package/dist-references/davinci-resolve-shortcuts.json +333 -0
- package/dist-references/devto.json +317 -0
- package/dist-references/discord.json +549 -0
- package/dist-references/figma.json +1186 -0
- package/dist-references/finder.json +146 -0
- package/dist-references/google-ads-transparency.json +95 -0
- package/dist-references/google-flow.json +649 -0
- package/dist-references/instagram.json +341 -0
- package/dist-references/linkedin.json +324 -0
- package/dist-references/meta-ad-library.json +86 -0
- package/dist-references/n8n.json +387 -0
- package/dist-references/notes.json +27 -0
- package/dist-references/notion.json +163 -0
- package/dist-references/reddit.json +341 -0
- package/dist-references/threads.json +337 -0
- package/dist-references/x-twitter.json +403 -0
- package/dist-references/youtube.json +373 -0
- package/native/macos-bridge/Package.swift +1 -0
- package/native/macos-bridge/Sources/AccessibilityBridge.swift +257 -36
- package/native/macos-bridge/Sources/AppManagement.swift +212 -2
- package/native/macos-bridge/Sources/CoreGraphicsBridge.swift +348 -53
- package/native/macos-bridge/Sources/StreamCapture.swift +136 -0
- package/native/macos-bridge/Sources/VisionBridge.swift +165 -7
- package/native/macos-bridge/Sources/main.swift +169 -16
- package/native/windows-bridge/Program.cs +5 -0
- package/native/windows-bridge/ScreenCapture.cs +124 -0
- package/package.json +29 -4
- package/scripts/postinstall.cjs +127 -0
- package/.claude/commands/automate.md +0 -28
- package/.claude/commands/debug-ui.md +0 -19
- package/.claude/commands/screenshot.md +0 -15
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.mcp.json +0 -8
- package/DESKTOP_MCP_GUIDE.md +0 -92
- package/SECURITY.md +0 -44
- package/docs/architecture.md +0 -47
- package/install-skills.sh +0 -19
- package/mcp-bridge.ts +0 -271
- package/mcp-desktop.ts +0 -1221
- package/playbooks/instagram.json +0 -41
- package/playbooks/instagram_v2.json +0 -201
- package/playbooks/x_v1.json +0 -211
- package/scripts/devpost-live-loop.mjs +0 -421
- package/src/logging/timeline-logger.ts +0 -55
- package/src/mcp/server.ts +0 -449
- package/src/memory/recall.ts +0 -191
- package/src/memory/research.ts +0 -146
- package/src/memory/seeds.ts +0 -123
- package/src/memory/session.ts +0 -201
- package/src/memory/store.ts +0 -434
- package/src/memory/types.ts +0 -69
- package/src/native/bridge-client.ts +0 -239
- package/src/runtime/accessibility-adapter.ts +0 -487
- package/src/runtime/app-adapter.ts +0 -169
- package/src/runtime/applescript-adapter.ts +0 -376
- package/src/runtime/ax-role-map.ts +0 -102
- package/src/runtime/browser-adapter.ts +0 -129
- package/src/runtime/cdp-chrome-adapter.ts +0 -676
- package/src/runtime/composite-adapter.ts +0 -274
- package/src/runtime/executor.ts +0 -396
- package/src/runtime/planning-loop.ts +0 -81
- package/src/runtime/service.ts +0 -448
- package/src/runtime/session-manager.ts +0 -50
- package/src/runtime/state-observer.ts +0 -136
- package/src/runtime/vision-adapter.ts +0 -297
- package/src/types.ts +0 -297
- package/tests/bridge-client.test.ts +0 -176
- package/tests/browser-stealth.test.ts +0 -210
- package/tests/composite-adapter.test.ts +0 -64
- package/tests/mcp-server.test.ts +0 -151
- package/tests/memory-recall.test.ts +0 -339
- package/tests/memory-research.test.ts +0 -159
- package/tests/memory-seeds.test.ts +0 -120
- package/tests/memory-store.test.ts +0 -392
- package/tests/types.test.ts +0 -92
- package/tsconfig.check.json +0 -17
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -8
- /package/{playbooks → dist-references}/devpost.json +0 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
/**
|
|
18
|
+
* MemoryService — unified facade over MemoryStore, SessionTracker, and RecallEngine
|
|
19
|
+
*
|
|
20
|
+
* Single entry-point for all memory operations. Adds:
|
|
21
|
+
* - state.json snapshot (debounced, written on every action)
|
|
22
|
+
* - learnings.jsonl (verified patterns, separate from strategies)
|
|
23
|
+
* - MemoryPolicy (error/stall thresholds)
|
|
24
|
+
* - Mission tracking
|
|
25
|
+
*/
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { writeFileAtomicSync } from "../util/atomic-write.js";
|
|
29
|
+
import { MemoryStore } from "./store.js";
|
|
30
|
+
import { SessionTracker } from "./session.js";
|
|
31
|
+
import { RecallEngine } from "./recall.js";
|
|
32
|
+
import { seedLearningsFromPlaybooks } from "./playbook-seeds.js";
|
|
33
|
+
// ── Defaults ─────────────────────────────────────
|
|
34
|
+
const DEFAULT_POLICY = {
|
|
35
|
+
maxConsecutiveErrors: 5,
|
|
36
|
+
stallThresholdMs: 300_000,
|
|
37
|
+
escalateAfterRetries: 3,
|
|
38
|
+
pauseBetweenActionsMs: 500,
|
|
39
|
+
};
|
|
40
|
+
const SNAPSHOT_DEBOUNCE_MS = 200;
|
|
41
|
+
const MAX_LEARNINGS = 1000;
|
|
42
|
+
// ── Service ──────────────────────────────────────
|
|
43
|
+
export class MemoryService {
|
|
44
|
+
store;
|
|
45
|
+
session;
|
|
46
|
+
recall;
|
|
47
|
+
baseDir;
|
|
48
|
+
memDir;
|
|
49
|
+
learningsCache = [];
|
|
50
|
+
snapshot;
|
|
51
|
+
snapshotTimer = null;
|
|
52
|
+
consecutiveErrors = 0;
|
|
53
|
+
lastError = null;
|
|
54
|
+
actionsFailed = 0;
|
|
55
|
+
actionsTotal = 0;
|
|
56
|
+
sessionStartedAt;
|
|
57
|
+
initialized = false;
|
|
58
|
+
constructor(baseDir) {
|
|
59
|
+
this.baseDir = baseDir;
|
|
60
|
+
this.memDir = path.join(baseDir, ".screenhand", "memory");
|
|
61
|
+
this.store = new MemoryStore(baseDir);
|
|
62
|
+
this.session = new SessionTracker(this.store);
|
|
63
|
+
this.recall = new RecallEngine(this.store);
|
|
64
|
+
this.sessionStartedAt = new Date().toISOString();
|
|
65
|
+
this.snapshot = {
|
|
66
|
+
session: {
|
|
67
|
+
id: this.session.getSessionId(),
|
|
68
|
+
client: "unknown",
|
|
69
|
+
startedAt: this.sessionStartedAt,
|
|
70
|
+
lastActionAt: this.sessionStartedAt,
|
|
71
|
+
},
|
|
72
|
+
mission: { current: null, phase: "idle" },
|
|
73
|
+
health: {
|
|
74
|
+
actionsTotal: 0,
|
|
75
|
+
actionsFailed: 0,
|
|
76
|
+
successRate: 1,
|
|
77
|
+
lastError: null,
|
|
78
|
+
consecutiveErrors: 0,
|
|
79
|
+
},
|
|
80
|
+
patterns: { topWorking: [], topFailing: [], knownBlockers: [] },
|
|
81
|
+
policy: { ...DEFAULT_POLICY },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ── Initialization ───────────────────────────────
|
|
85
|
+
/** Load all caches from disk. Call once at startup. */
|
|
86
|
+
init() {
|
|
87
|
+
if (this.initialized)
|
|
88
|
+
return;
|
|
89
|
+
this.initialized = true;
|
|
90
|
+
this.store.init();
|
|
91
|
+
this.ensureMemDir();
|
|
92
|
+
// Load learnings
|
|
93
|
+
this.learningsCache = this.readJsonlSafe("learnings.jsonl");
|
|
94
|
+
// Seed learnings from playbooks — selectors, detection, policy notes.
|
|
95
|
+
// These have scope prefixes like "chrome/figma" so they merge cleanly
|
|
96
|
+
// with runtime-discovered learnings without duplicating.
|
|
97
|
+
const playbooksDir = path.join(this.baseDir, "references");
|
|
98
|
+
const pbLearnings = seedLearningsFromPlaybooks(playbooksDir);
|
|
99
|
+
if (pbLearnings.length > 0) {
|
|
100
|
+
const existingKeys = new Set(this.learningsCache.map(l => `${l.scope}::${l.pattern}::${l.method}`));
|
|
101
|
+
let added = 0;
|
|
102
|
+
for (const pl of pbLearnings) {
|
|
103
|
+
const key = `${pl.scope}::${pl.pattern}::${pl.method}`;
|
|
104
|
+
if (!existingKeys.has(key)) {
|
|
105
|
+
this.learningsCache.push({
|
|
106
|
+
id: `pb_lrn_${added}`,
|
|
107
|
+
...pl,
|
|
108
|
+
});
|
|
109
|
+
existingKeys.add(key);
|
|
110
|
+
added++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Load existing snapshot if present (to restore mission/policy across restarts)
|
|
115
|
+
const snapPath = this.filePath("state.json");
|
|
116
|
+
if (fs.existsSync(snapPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = fs.readFileSync(snapPath, "utf-8");
|
|
119
|
+
const loaded = JSON.parse(raw);
|
|
120
|
+
// Restore mission and policy from previous run
|
|
121
|
+
if (loaded.mission)
|
|
122
|
+
this.snapshot.mission = loaded.mission;
|
|
123
|
+
if (loaded.policy)
|
|
124
|
+
this.snapshot.policy = { ...DEFAULT_POLICY, ...loaded.policy };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Corrupted snapshot — start fresh
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Sync stats from store
|
|
131
|
+
const stats = this.store.getStats();
|
|
132
|
+
this.actionsTotal = stats.totalActions;
|
|
133
|
+
this.actionsFailed = stats.totalActions - Math.round(stats.successRate * stats.totalActions);
|
|
134
|
+
this.rebuildPatterns();
|
|
135
|
+
this.updateHealthInSnapshot();
|
|
136
|
+
this.writeSnapshotSync();
|
|
137
|
+
}
|
|
138
|
+
// ── Snapshot ─────────────────────────────────────
|
|
139
|
+
/** Get the current in-memory snapshot (zero-cost). */
|
|
140
|
+
getSnapshot() {
|
|
141
|
+
return this.snapshot;
|
|
142
|
+
}
|
|
143
|
+
scheduleSnapshotWrite() {
|
|
144
|
+
if (this.snapshotTimer)
|
|
145
|
+
return;
|
|
146
|
+
this.snapshotTimer = setTimeout(() => {
|
|
147
|
+
this.snapshotTimer = null;
|
|
148
|
+
this.writeSnapshotSync();
|
|
149
|
+
}, SNAPSHOT_DEBOUNCE_MS);
|
|
150
|
+
}
|
|
151
|
+
writeSnapshotSync() {
|
|
152
|
+
this.ensureMemDir();
|
|
153
|
+
try {
|
|
154
|
+
writeFileAtomicSync(this.filePath("state.json"), JSON.stringify(this.snapshot, null, 2));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Non-critical
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
updateHealthInSnapshot() {
|
|
161
|
+
this.snapshot.health = {
|
|
162
|
+
actionsTotal: this.actionsTotal,
|
|
163
|
+
actionsFailed: this.actionsFailed,
|
|
164
|
+
successRate: this.actionsTotal > 0 ? (this.actionsTotal - this.actionsFailed) / this.actionsTotal : 1,
|
|
165
|
+
lastError: this.lastError,
|
|
166
|
+
consecutiveErrors: this.consecutiveErrors,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
rebuildPatterns() {
|
|
170
|
+
// Top working: learnings with high confidence, sorted desc
|
|
171
|
+
const working = this.learningsCache
|
|
172
|
+
.filter((l) => l.confidence >= 0.6 && l.successCount > l.failCount)
|
|
173
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
174
|
+
.slice(0, 10)
|
|
175
|
+
.map((l) => `${l.scope}: ${l.pattern} (${l.method})`);
|
|
176
|
+
// Top failing: learnings with low confidence or high fail count
|
|
177
|
+
const failing = this.learningsCache
|
|
178
|
+
.filter((l) => l.failCount > l.successCount)
|
|
179
|
+
.sort((a, b) => b.failCount - a.failCount)
|
|
180
|
+
.slice(0, 10)
|
|
181
|
+
.map((l) => `${l.scope}: ${l.pattern} (${l.method})`);
|
|
182
|
+
// Known blockers: errors with no resolution
|
|
183
|
+
const errors = this.store.readErrors();
|
|
184
|
+
const blockers = errors
|
|
185
|
+
.filter((e) => !e.resolution && e.occurrences >= 2)
|
|
186
|
+
.sort((a, b) => b.occurrences - a.occurrences)
|
|
187
|
+
.slice(0, 10)
|
|
188
|
+
.map((e) => `${e.tool}: ${e.error}`);
|
|
189
|
+
this.snapshot.patterns = {
|
|
190
|
+
topWorking: working,
|
|
191
|
+
topFailing: failing,
|
|
192
|
+
knownBlockers: blockers,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// ── Recording ────────────────────────────────────
|
|
196
|
+
/** Record an action event. Delegates to store + session tracker, updates snapshot. */
|
|
197
|
+
recordEvent(entry) {
|
|
198
|
+
this.store.appendAction(entry);
|
|
199
|
+
this.session.recordAction(entry);
|
|
200
|
+
this.actionsTotal++;
|
|
201
|
+
if (!entry.success) {
|
|
202
|
+
this.actionsFailed++;
|
|
203
|
+
this.consecutiveErrors++;
|
|
204
|
+
this.lastError = entry.error;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.consecutiveErrors = 0;
|
|
208
|
+
}
|
|
209
|
+
this.snapshot.session.id = this.session.getSessionId();
|
|
210
|
+
this.snapshot.session.lastActionAt = entry.timestamp;
|
|
211
|
+
this.updateHealthInSnapshot();
|
|
212
|
+
this.scheduleSnapshotWrite();
|
|
213
|
+
}
|
|
214
|
+
/** Record an error pattern. Delegates to store, optionally creates a learning. */
|
|
215
|
+
recordError(tool, error, fix, scope) {
|
|
216
|
+
const pattern = {
|
|
217
|
+
id: "err_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
218
|
+
tool,
|
|
219
|
+
params: {},
|
|
220
|
+
error,
|
|
221
|
+
resolution: fix,
|
|
222
|
+
occurrences: 1,
|
|
223
|
+
lastSeen: new Date().toISOString(),
|
|
224
|
+
};
|
|
225
|
+
this.store.appendError(pattern);
|
|
226
|
+
// If a fix is provided, record it as a learning
|
|
227
|
+
if (fix && scope) {
|
|
228
|
+
this.recordLearning({
|
|
229
|
+
scope,
|
|
230
|
+
pattern: error,
|
|
231
|
+
method: "ax",
|
|
232
|
+
confidence: 0.5,
|
|
233
|
+
successCount: 0,
|
|
234
|
+
failCount: 1,
|
|
235
|
+
lastSeen: new Date().toISOString(),
|
|
236
|
+
fix,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
this.rebuildPatterns();
|
|
240
|
+
this.scheduleSnapshotWrite();
|
|
241
|
+
}
|
|
242
|
+
// ── Learnings ────────────────────────────────────
|
|
243
|
+
/** Append a verified learning to learnings.jsonl. */
|
|
244
|
+
recordLearning(learning) {
|
|
245
|
+
const full = {
|
|
246
|
+
id: "lrn_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
247
|
+
...learning,
|
|
248
|
+
};
|
|
249
|
+
// Check for existing learning with same scope + pattern + method
|
|
250
|
+
const idx = this.learningsCache.findIndex((l) => l.scope === full.scope && l.pattern === full.pattern && l.method === full.method);
|
|
251
|
+
if (idx >= 0) {
|
|
252
|
+
const existing = this.learningsCache[idx];
|
|
253
|
+
this.learningsCache[idx] = {
|
|
254
|
+
...existing,
|
|
255
|
+
successCount: existing.successCount + full.successCount,
|
|
256
|
+
failCount: existing.failCount + full.failCount,
|
|
257
|
+
confidence: this.computeConfidence(existing.successCount + full.successCount, existing.failCount + full.failCount),
|
|
258
|
+
lastSeen: full.lastSeen,
|
|
259
|
+
fix: full.fix ?? existing.fix,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
this.learningsCache.push(full);
|
|
264
|
+
this.enforceLearningsLimit();
|
|
265
|
+
}
|
|
266
|
+
this.writeLearningsAsync();
|
|
267
|
+
this.rebuildPatterns();
|
|
268
|
+
this.scheduleSnapshotWrite();
|
|
269
|
+
}
|
|
270
|
+
/** Search learnings by scope and/or method. */
|
|
271
|
+
queryPatterns(scope, method) {
|
|
272
|
+
let results = this.learningsCache;
|
|
273
|
+
if (scope) {
|
|
274
|
+
results = results.filter((l) => l.scope === scope || l.scope.startsWith(scope + "/"));
|
|
275
|
+
}
|
|
276
|
+
if (method) {
|
|
277
|
+
results = results.filter((l) => l.method === method);
|
|
278
|
+
}
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
computeConfidence(success, fail) {
|
|
282
|
+
const total = success + fail;
|
|
283
|
+
if (total === 0)
|
|
284
|
+
return 0;
|
|
285
|
+
return success / total;
|
|
286
|
+
}
|
|
287
|
+
enforceLearningsLimit() {
|
|
288
|
+
if (this.learningsCache.length <= MAX_LEARNINGS)
|
|
289
|
+
return;
|
|
290
|
+
// Evict lowest-confidence, oldest learnings
|
|
291
|
+
this.learningsCache.sort((a, b) => {
|
|
292
|
+
const confDiff = a.confidence - b.confidence;
|
|
293
|
+
if (Math.abs(confDiff) > 0.1)
|
|
294
|
+
return confDiff;
|
|
295
|
+
return new Date(a.lastSeen).getTime() - new Date(b.lastSeen).getTime();
|
|
296
|
+
});
|
|
297
|
+
this.learningsCache = this.learningsCache.slice(-MAX_LEARNINGS);
|
|
298
|
+
}
|
|
299
|
+
writeLearningsAsync() {
|
|
300
|
+
this.ensureMemDir();
|
|
301
|
+
const data = this.learningsCache.map((l) => JSON.stringify(l)).join("\n") + (this.learningsCache.length ? "\n" : "");
|
|
302
|
+
fs.writeFile(this.filePath("learnings.jsonl"), data, () => { });
|
|
303
|
+
}
|
|
304
|
+
// ── Recall (delegates to RecallEngine) ───────────
|
|
305
|
+
/** Search error patterns, optionally filtered by tool. */
|
|
306
|
+
queryErrors(tool) {
|
|
307
|
+
return this.recall.recallErrors(tool);
|
|
308
|
+
}
|
|
309
|
+
/** Fuzzy-match strategies by query string. Optionally filter by current app. */
|
|
310
|
+
recallStrategies(query, limit, currentBundleId) {
|
|
311
|
+
return this.recall.recallStrategies(query, limit, currentBundleId);
|
|
312
|
+
}
|
|
313
|
+
/** Quick error check for interceptor (~0ms). */
|
|
314
|
+
quickErrorCheck(tool) {
|
|
315
|
+
return this.recall.quickErrorCheck(tool);
|
|
316
|
+
}
|
|
317
|
+
/** Quick strategy hint for interceptor (~0ms). */
|
|
318
|
+
quickStrategyHint(recentTools, currentBundleId) {
|
|
319
|
+
return this.recall.quickStrategyHint(recentTools, currentBundleId);
|
|
320
|
+
}
|
|
321
|
+
/** Record strategy outcome for feedback loop. */
|
|
322
|
+
recordStrategyOutcome(fingerprint, success) {
|
|
323
|
+
this.store.recordStrategyOutcome(fingerprint, success);
|
|
324
|
+
}
|
|
325
|
+
// ── Session / Strategy ───────────────────────────
|
|
326
|
+
/** Get the current session ID. */
|
|
327
|
+
getSessionId() {
|
|
328
|
+
return this.session.getSessionId();
|
|
329
|
+
}
|
|
330
|
+
/** Get recent tool names from session buffer. */
|
|
331
|
+
getRecentToolNames(limit) {
|
|
332
|
+
return this.session.getRecentToolNames(limit);
|
|
333
|
+
}
|
|
334
|
+
/** End current session and save a strategy if successful. */
|
|
335
|
+
saveStrategy(task, tags) {
|
|
336
|
+
const strategy = this.session.endSession(true, task);
|
|
337
|
+
if (strategy && tags && tags.length > 0) {
|
|
338
|
+
// Merge additional tags
|
|
339
|
+
const merged = new Set([...strategy.tags, ...tags]);
|
|
340
|
+
strategy.tags = [...merged];
|
|
341
|
+
}
|
|
342
|
+
return strategy;
|
|
343
|
+
}
|
|
344
|
+
/** Read raw actions from store (for exports/playbooks). */
|
|
345
|
+
readActions() {
|
|
346
|
+
return this.store.readActions();
|
|
347
|
+
}
|
|
348
|
+
/** Read raw errors from store. */
|
|
349
|
+
readErrors() {
|
|
350
|
+
return this.store.readErrors();
|
|
351
|
+
}
|
|
352
|
+
/** Read raw strategies from store. */
|
|
353
|
+
readStrategies() {
|
|
354
|
+
return this.store.readStrategies();
|
|
355
|
+
}
|
|
356
|
+
/** Append an error pattern directly (for interceptor compatibility). */
|
|
357
|
+
appendError(pattern) {
|
|
358
|
+
this.store.appendError(pattern);
|
|
359
|
+
}
|
|
360
|
+
/** Append a strategy directly. */
|
|
361
|
+
appendStrategy(strategy) {
|
|
362
|
+
this.store.appendStrategy(strategy);
|
|
363
|
+
}
|
|
364
|
+
// ── Stats ────────────────────────────────────────
|
|
365
|
+
/** Get aggregate memory stats. */
|
|
366
|
+
getStats() {
|
|
367
|
+
return this.store.getStats();
|
|
368
|
+
}
|
|
369
|
+
// ── Mission ──────────────────────────────────────
|
|
370
|
+
/** Set the current mission and optionally a phase. */
|
|
371
|
+
setMission(mission, phase) {
|
|
372
|
+
this.snapshot.mission.current = mission;
|
|
373
|
+
if (phase)
|
|
374
|
+
this.snapshot.mission.phase = phase;
|
|
375
|
+
this.scheduleSnapshotWrite();
|
|
376
|
+
}
|
|
377
|
+
/** Set the client identifier (e.g., "claude-code", "mcp-desktop"). */
|
|
378
|
+
setClient(client) {
|
|
379
|
+
this.snapshot.session.client = client;
|
|
380
|
+
this.scheduleSnapshotWrite();
|
|
381
|
+
}
|
|
382
|
+
// ── Clear ────────────────────────────────────────
|
|
383
|
+
/** Clear specific memory categories or everything. */
|
|
384
|
+
clear(what) {
|
|
385
|
+
if (what === "learnings" || what === "all") {
|
|
386
|
+
this.learningsCache = [];
|
|
387
|
+
const fp = this.filePath("learnings.jsonl");
|
|
388
|
+
if (fs.existsSync(fp))
|
|
389
|
+
fs.writeFileSync(fp, "");
|
|
390
|
+
}
|
|
391
|
+
if (what !== "learnings") {
|
|
392
|
+
// Delegate non-learnings clears to the store
|
|
393
|
+
const storeWhat = what === "all" ? "all" : what;
|
|
394
|
+
this.store.clear(storeWhat);
|
|
395
|
+
}
|
|
396
|
+
if (what === "all" || what === "actions") {
|
|
397
|
+
this.actionsTotal = 0;
|
|
398
|
+
this.actionsFailed = 0;
|
|
399
|
+
this.consecutiveErrors = 0;
|
|
400
|
+
this.lastError = null;
|
|
401
|
+
}
|
|
402
|
+
this.updateHealthInSnapshot();
|
|
403
|
+
this.rebuildPatterns();
|
|
404
|
+
this.writeSnapshotSync();
|
|
405
|
+
}
|
|
406
|
+
// ── Helpers ──────────────────────────────────────
|
|
407
|
+
ensureMemDir() {
|
|
408
|
+
if (!fs.existsSync(this.memDir)) {
|
|
409
|
+
fs.mkdirSync(this.memDir, { recursive: true });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
filePath(name) {
|
|
413
|
+
return path.join(this.memDir, name);
|
|
414
|
+
}
|
|
415
|
+
readJsonlSafe(file) {
|
|
416
|
+
const fp = this.filePath(file);
|
|
417
|
+
if (!fs.existsSync(fp))
|
|
418
|
+
return [];
|
|
419
|
+
let text;
|
|
420
|
+
try {
|
|
421
|
+
// Guard against oversized files (same 10MB limit as LearningEngine)
|
|
422
|
+
const stat = fs.statSync(fp);
|
|
423
|
+
if (stat.size > 10 * 1024 * 1024)
|
|
424
|
+
return [];
|
|
425
|
+
text = fs.readFileSync(fp, "utf-8").trim();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
if (!text)
|
|
431
|
+
return [];
|
|
432
|
+
const results = [];
|
|
433
|
+
for (const line of text.split("\n")) {
|
|
434
|
+
const trimmed = line.trim();
|
|
435
|
+
if (!trimmed)
|
|
436
|
+
continue;
|
|
437
|
+
try {
|
|
438
|
+
results.push(JSON.parse(trimmed));
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Skip corrupted line
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return results;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// Copyright (C) 2025 Clazro Technology Private Limited
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
//
|
|
4
|
+
// This file is part of ScreenHand.
|
|
5
|
+
//
|
|
6
|
+
// ScreenHand is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as
|
|
8
|
+
// published by the Free Software Foundation, version 3.
|
|
9
|
+
//
|
|
10
|
+
// ScreenHand is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with ScreenHand. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
import { MemoryStore } from "./store.js";
|
|
18
|
+
const SESSION_GAP_MS = 60_000; // 60s gap = new session
|
|
19
|
+
const MAX_BUFFER_SIZE = 100;
|
|
20
|
+
const MIN_AUTO_SAVE_STEPS = 3; // Need at least 3 successful steps to auto-save
|
|
21
|
+
export class SessionTracker {
|
|
22
|
+
store;
|
|
23
|
+
sessionId;
|
|
24
|
+
taskDescription = null;
|
|
25
|
+
buffer = [];
|
|
26
|
+
lastActionTime = 0;
|
|
27
|
+
constructor(store) {
|
|
28
|
+
this.store = store;
|
|
29
|
+
this.sessionId = SessionTracker.generateId();
|
|
30
|
+
}
|
|
31
|
+
static generateId() {
|
|
32
|
+
return "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
33
|
+
}
|
|
34
|
+
/** Start (or restart) a named task session */
|
|
35
|
+
startSession(taskDescription) {
|
|
36
|
+
// Auto-save previous session if it had successful actions
|
|
37
|
+
this.tryAutoSave();
|
|
38
|
+
this.sessionId = SessionTracker.generateId();
|
|
39
|
+
this.taskDescription = taskDescription ?? null;
|
|
40
|
+
this.buffer = [];
|
|
41
|
+
this.lastActionTime = Date.now();
|
|
42
|
+
return this.sessionId;
|
|
43
|
+
}
|
|
44
|
+
/** Get the current session ID, auto-rotating if stale */
|
|
45
|
+
getSessionId() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (this.lastActionTime > 0 && now - this.lastActionTime > SESSION_GAP_MS) {
|
|
48
|
+
// Session gap detected — auto-save previous sequence then start fresh
|
|
49
|
+
this.tryAutoSave();
|
|
50
|
+
this.sessionId = SessionTracker.generateId();
|
|
51
|
+
this.buffer = [];
|
|
52
|
+
this.taskDescription = null;
|
|
53
|
+
}
|
|
54
|
+
return this.sessionId;
|
|
55
|
+
}
|
|
56
|
+
/** Record an action into the current session buffer */
|
|
57
|
+
recordAction(entry) {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (this.lastActionTime > 0 && now - this.lastActionTime > SESSION_GAP_MS) {
|
|
60
|
+
// Gap detected — auto-save then start new session
|
|
61
|
+
this.tryAutoSave();
|
|
62
|
+
this.sessionId = SessionTracker.generateId();
|
|
63
|
+
this.buffer = [];
|
|
64
|
+
this.taskDescription = null;
|
|
65
|
+
}
|
|
66
|
+
this.lastActionTime = now;
|
|
67
|
+
this.buffer.push(entry);
|
|
68
|
+
if (this.buffer.length > MAX_BUFFER_SIZE) {
|
|
69
|
+
this.buffer.shift();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** End the session and save a strategy if successful */
|
|
73
|
+
endSession(success, taskDescription) {
|
|
74
|
+
const task = taskDescription ?? this.taskDescription;
|
|
75
|
+
if (!success || !task || this.buffer.length === 0) {
|
|
76
|
+
this.buffer = [];
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const strategy = this.buildStrategy(task, this.buffer);
|
|
80
|
+
this.store.appendStrategy(strategy);
|
|
81
|
+
this.buffer = [];
|
|
82
|
+
return strategy;
|
|
83
|
+
}
|
|
84
|
+
/** Get the current session's action buffer */
|
|
85
|
+
getBuffer() {
|
|
86
|
+
return [...this.buffer];
|
|
87
|
+
}
|
|
88
|
+
/** Get recent tool names (for strategy hint matching) */
|
|
89
|
+
getRecentToolNames(limit = 10) {
|
|
90
|
+
return this.buffer.slice(-limit).map((a) => a.tool);
|
|
91
|
+
}
|
|
92
|
+
/** Get current task description */
|
|
93
|
+
getTaskDescription() {
|
|
94
|
+
return this.taskDescription;
|
|
95
|
+
}
|
|
96
|
+
// ── auto-save logic ────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Try to auto-save the current buffer as a strategy.
|
|
99
|
+
* Only saves if there are MIN_AUTO_SAVE_STEPS+ consecutive successes.
|
|
100
|
+
* Uses tool sequence as task description if no explicit one was given.
|
|
101
|
+
*/
|
|
102
|
+
tryAutoSave() {
|
|
103
|
+
if (this.buffer.length < MIN_AUTO_SAVE_STEPS)
|
|
104
|
+
return;
|
|
105
|
+
// Find the longest trailing streak of successes
|
|
106
|
+
let successStreak = [];
|
|
107
|
+
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
|
108
|
+
if (this.buffer[i].success) {
|
|
109
|
+
successStreak.unshift(this.buffer[i]);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (successStreak.length < MIN_AUTO_SAVE_STEPS)
|
|
116
|
+
return;
|
|
117
|
+
// Build a task description from the tool sequence if none provided
|
|
118
|
+
const task = this.taskDescription ?? this.inferTaskDescription(successStreak);
|
|
119
|
+
const strategy = this.buildStrategy(task, successStreak);
|
|
120
|
+
this.store.appendStrategy(strategy);
|
|
121
|
+
}
|
|
122
|
+
/** Infer a task description from a sequence of actions */
|
|
123
|
+
inferTaskDescription(actions) {
|
|
124
|
+
const tools = [...new Set(actions.map((a) => a.tool))];
|
|
125
|
+
// Extract key param values (bundle IDs, titles, URLs, etc.)
|
|
126
|
+
const keyParams = [];
|
|
127
|
+
for (const a of actions) {
|
|
128
|
+
for (const [key, val] of Object.entries(a.params)) {
|
|
129
|
+
if (typeof val === "string" && val.length > 2 && val.length < 60) {
|
|
130
|
+
if (["bundleId", "title", "url", "text", "script", "selector", "menuPath"].includes(key)) {
|
|
131
|
+
keyParams.push(val);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const paramHint = keyParams.length > 0 ? ` (${keyParams.slice(0, 3).join(", ")})` : "";
|
|
137
|
+
return `${tools.join(" → ")}${paramHint}`;
|
|
138
|
+
}
|
|
139
|
+
buildStrategy(task, actions) {
|
|
140
|
+
const steps = actions.map((a) => ({
|
|
141
|
+
tool: a.tool,
|
|
142
|
+
params: a.params,
|
|
143
|
+
}));
|
|
144
|
+
const totalDurationMs = actions.reduce((sum, a) => sum + a.durationMs, 0);
|
|
145
|
+
const tags = extractTags(task, steps);
|
|
146
|
+
const fingerprint = MemoryStore.makeFingerprint(steps.map((s) => s.tool));
|
|
147
|
+
return {
|
|
148
|
+
id: "str_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
149
|
+
task,
|
|
150
|
+
steps,
|
|
151
|
+
totalDurationMs,
|
|
152
|
+
successCount: 1,
|
|
153
|
+
failCount: 0,
|
|
154
|
+
lastUsed: new Date().toISOString(),
|
|
155
|
+
tags,
|
|
156
|
+
fingerprint,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Extract tags from task description and tool names */
|
|
161
|
+
function extractTags(task, steps) {
|
|
162
|
+
const tags = new Set();
|
|
163
|
+
const words = task.toLowerCase().split(/\W+/).filter((w) => w.length >= 3);
|
|
164
|
+
for (const w of words)
|
|
165
|
+
tags.add(w);
|
|
166
|
+
for (const s of steps)
|
|
167
|
+
tags.add(s.tool);
|
|
168
|
+
return [...tags];
|
|
169
|
+
}
|