contextos-agents 1.7.0 → 2.0.0-beta.3
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/.agents/AGENTS.md +16 -50
- package/.agents/adapters/aider/export.js +117 -99
- package/.agents/adapters/claude/export.js +68 -26
- package/.agents/adapters/copilot/export.js +90 -53
- package/.agents/adapters/cursor/export.js +100 -104
- package/.agents/adapters/drift-detector.js +196 -0
- package/.agents/adapters/gemini/export.js +76 -45
- package/.agents/adapters/pure-compiler.js +443 -0
- package/.agents/adapters/zed/export.js +104 -96
- package/.agents/compiled/registry.v2.json +504 -0
- package/.agents/compiled/registry.v2.sha256 +1 -0
- package/.agents/compiler/manifest-compiler.js +963 -0
- package/.agents/compiler/vendor/yaml.LICENSE.txt +13 -0
- package/.agents/compiler/vendor/yaml.SBOM.json +6 -0
- package/.agents/compiler/vendor/yaml.js +139 -0
- package/.agents/core/profiles/init.yaml +25 -0
- package/.agents/core/skills/context-manager/skill.yaml +3 -5
- package/.agents/core/skills/context-os/SKILL.md +3 -6
- package/.agents/core/skills/context-os/skill.yaml +3 -8
- package/.agents/core/skills/engineering-workflow/skill.yaml +1 -7
- package/.agents/core/skills/gemini-precision/skill.yaml +1 -6
- package/.agents/core/skills/gstack-roles/skill.yaml +3 -6
- package/.agents/core/skills/ponytail-mindset/skill.yaml +1 -7
- package/.agents/core/skills/security/skill.yaml +15 -3
- package/.agents/ctx.js +537 -112
- package/.agents/customization-dx.js +282 -0
- package/.agents/doctor.js +855 -66
- package/.agents/filesystem/index.js +71 -0
- package/.agents/filesystem/journaled-transaction.js +451 -0
- package/.agents/filesystem/lockfile-v2.js +275 -0
- package/.agents/filesystem/platform-hardening.js +222 -0
- package/.agents/filesystem/project-lock.js +218 -0
- package/.agents/filesystem/safe-path.js +256 -0
- package/.agents/generated/claude/skills/context-os/SKILL.md +1 -1
- package/.agents/generated/gemini/skills/context-os/SKILL.md +2 -2
- package/.agents/plugins/contextos/plugin.json +1 -1
- package/.agents/plugins.js +278 -64
- package/.agents/profiles.js +486 -51
- package/.agents/resolver/canonical-resolver.js +1348 -0
- package/.agents/resolver.js +50 -534
- package/.agents/rules/rule-catalog.js +525 -0
- package/.agents/schemas/attestation.review.v1.json +111 -0
- package/.agents/schemas/attestation.verification.v1.json +85 -0
- package/.agents/schemas/lockfile.v2.schema.json +134 -0
- package/.agents/schemas/profile.v2.schema.json +114 -0
- package/.agents/schemas/runtime.thread.v1.json +192 -0
- package/.agents/schemas/skill.manifest.v2.json +177 -0
- package/.agents/schemas/verification.spec.v1.json +39 -0
- package/.agents/schemas/workspace.graph.schema.json +106 -0
- package/.agents/skills-index.json +6 -166
- package/.agents/stats.js +9 -9
- package/.agents/transaction-core/event-store.js +288 -0
- package/.agents/transaction-core/idempotency.js +129 -0
- package/.agents/transaction-core/ipc-lock.js +311 -0
- package/.agents/transaction-core/plugin-supply-chain-bundle.js +436 -0
- package/.agents/validate.js +44 -1
- package/.agents/watch.js +354 -102
- package/.agents/workspace/workspace-graph.js +778 -0
- package/README.md +59 -387
- package/benchmarks/v2/analysis/statistics.js +140 -0
- package/benchmarks/v2/analysis/stats.js +69 -0
- package/benchmarks/v2/arms/arm-definitions.js +79 -0
- package/benchmarks/v2/dataset.schema.json +34 -0
- package/benchmarks/v2/evaluators/index.js +25 -0
- package/benchmarks/v2/evaluators/verified-success.js +116 -0
- package/benchmarks/v2/harness/runner.js +88 -0
- package/benchmarks/v2/pilot-tasks.json +392 -0
- package/bin/commands/recover.js +88 -0
- package/bin/commands/update.js +80 -17
- package/bin/commands.js +62 -25
- package/bin/index.js +138 -81
- package/bin/lib/lockfile.js +5 -3
- package/bin/lib/safe-writer.js +34 -3
- package/package.json +87 -72
- package/registry.json +2 -2
- package/registry.v2.schema.json +86 -0
- package/.agents/core/profiles/backend.yaml +0 -47
- package/.agents/core/profiles/enterprise.yaml +0 -46
- package/.agents/core/profiles/frontend.yaml +0 -46
- package/.agents/core/profiles/hackathon.yaml +0 -45
- package/.agents/core/profiles/mvp.yaml +0 -44
- package/.agents/core/profiles/startup.yaml +0 -48
- package/.agents/core/skills/adapters/EXAMPLES.md +0 -19
- package/.agents/core/skills/adapters/SKILL.md +0 -105
- package/.agents/core/skills/adapters/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/adapters/VALIDATION.json +0 -12
- package/.agents/core/skills/adapters/skill.yaml +0 -16
- package/.agents/core/skills/architecture-diagrams/SKILL.md +0 -108
- package/.agents/core/skills/architecture-diagrams/VALIDATION.json +0 -12
- package/.agents/core/skills/architecture-diagrams/skill.yaml +0 -12
- package/.agents/core/skills/brutalist-design/SKILL.md +0 -150
- package/.agents/core/skills/brutalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/brutalist-design/skill.yaml +0 -12
- package/.agents/core/skills/database/EXAMPLES.md +0 -74
- package/.agents/core/skills/database/SKILL.md +0 -101
- package/.agents/core/skills/database/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/database/VALIDATION.json +0 -11
- package/.agents/core/skills/database/skill.yaml +0 -31
- package/.agents/core/skills/ddd/EXAMPLES.md +0 -42
- package/.agents/core/skills/ddd/SKILL.md +0 -247
- package/.agents/core/skills/ddd/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ddd/VALIDATION.json +0 -12
- package/.agents/core/skills/ddd/ddd.md +0 -178
- package/.agents/core/skills/ddd/skill.yaml +0 -17
- package/.agents/core/skills/decisions/EXAMPLES.md +0 -35
- package/.agents/core/skills/decisions/SKILL.md +0 -90
- package/.agents/core/skills/decisions/TROUBLESHOOTING.md +0 -13
- package/.agents/core/skills/decisions/VALIDATION.json +0 -12
- package/.agents/core/skills/decisions/skill.yaml +0 -16
- package/.agents/core/skills/docker/EXAMPLES.md +0 -56
- package/.agents/core/skills/docker/SKILL.md +0 -63
- package/.agents/core/skills/docker/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/docker/VALIDATION.json +0 -11
- package/.agents/core/skills/docker/skill.yaml +0 -29
- package/.agents/core/skills/fastapi/EXAMPLES.md +0 -36
- package/.agents/core/skills/fastapi/SKILL.md +0 -148
- package/.agents/core/skills/fastapi/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/fastapi/VALIDATION.json +0 -12
- package/.agents/core/skills/fastapi/fastapi.md +0 -112
- package/.agents/core/skills/fastapi/skill.yaml +0 -17
- package/.agents/core/skills/generators/EXAMPLES.md +0 -19
- package/.agents/core/skills/generators/SKILL.md +0 -112
- package/.agents/core/skills/generators/TROUBLESHOOTING.md +0 -7
- package/.agents/core/skills/generators/VALIDATION.json +0 -12
- package/.agents/core/skills/generators/skill.yaml +0 -25
- package/.agents/core/skills/generators/templates/API.md +0 -77
- package/.agents/core/skills/generators/templates/ARCHITECTURE.md +0 -70
- package/.agents/core/skills/generators/templates/DATABASE.md +0 -42
- package/.agents/core/skills/generators/templates/DECISION.md +0 -46
- package/.agents/core/skills/generators/templates/PRD.md +0 -67
- package/.agents/core/skills/generators/templates/PROJECT_GRAPH.md +0 -56
- package/.agents/core/skills/generators/templates/ROADMAP.md +0 -51
- package/.agents/core/skills/generators/templates/TASKS.md +0 -43
- package/.agents/core/skills/generators/templates/UI.md +0 -73
- package/.agents/core/skills/graphify/EXAMPLES.md +0 -73
- package/.agents/core/skills/graphify/SKILL.md +0 -130
- package/.agents/core/skills/graphify/VALIDATION.json +0 -12
- package/.agents/core/skills/graphify/skill.yaml +0 -18
- package/.agents/core/skills/impeccable-design/EXAMPLES.md +0 -26
- package/.agents/core/skills/impeccable-design/SKILL.md +0 -201
- package/.agents/core/skills/impeccable-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/impeccable-design/VALIDATION.json +0 -12
- package/.agents/core/skills/impeccable-design/skill.yaml +0 -20
- package/.agents/core/skills/interview-me/SKILL.md +0 -97
- package/.agents/core/skills/interview-me/VALIDATION.json +0 -12
- package/.agents/core/skills/interview-me/skill.yaml +0 -12
- package/.agents/core/skills/microservices/EXAMPLES.md +0 -38
- package/.agents/core/skills/microservices/SKILL.md +0 -164
- package/.agents/core/skills/microservices/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/microservices/VALIDATION.json +0 -12
- package/.agents/core/skills/microservices/microservices.md +0 -119
- package/.agents/core/skills/microservices/skill.yaml +0 -17
- package/.agents/core/skills/minimalist-design/SKILL.md +0 -113
- package/.agents/core/skills/minimalist-design/VALIDATION.json +0 -12
- package/.agents/core/skills/minimalist-design/skill.yaml +0 -12
- package/.agents/core/skills/nestjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nestjs/SKILL.md +0 -139
- package/.agents/core/skills/nestjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nestjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nestjs/nestjs.md +0 -103
- package/.agents/core/skills/nestjs/skill.yaml +0 -17
- package/.agents/core/skills/nextjs/EXAMPLES.md +0 -40
- package/.agents/core/skills/nextjs/SKILL.md +0 -163
- package/.agents/core/skills/nextjs/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/nextjs/VALIDATION.json +0 -12
- package/.agents/core/skills/nextjs/nextjs.md +0 -67
- package/.agents/core/skills/nextjs/skill.yaml +0 -17
- package/.agents/core/skills/node/EXAMPLES.md +0 -80
- package/.agents/core/skills/node/SKILL.md +0 -128
- package/.agents/core/skills/node/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/node/VALIDATION.json +0 -12
- package/.agents/core/skills/node/node.md +0 -87
- package/.agents/core/skills/node/skill.yaml +0 -17
- package/.agents/core/skills/performance/EXAMPLES.md +0 -30
- package/.agents/core/skills/performance/SKILL.md +0 -75
- package/.agents/core/skills/performance/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/performance/VALIDATION.json +0 -12
- package/.agents/core/skills/performance/performance.md +0 -52
- package/.agents/core/skills/performance/skill.yaml +0 -17
- package/.agents/core/skills/react/EXAMPLES.md +0 -79
- package/.agents/core/skills/react/SKILL.md +0 -132
- package/.agents/core/skills/react/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/react/VALIDATION.json +0 -12
- package/.agents/core/skills/react/react.md +0 -93
- package/.agents/core/skills/react/skill.yaml +0 -17
- package/.agents/core/skills/react-best-practices/SKILL.md +0 -155
- package/.agents/core/skills/react-best-practices/VALIDATION.json +0 -12
- package/.agents/core/skills/react-best-practices/skill.yaml +0 -14
- package/.agents/core/skills/redesign-audit/SKILL.md +0 -117
- package/.agents/core/skills/redesign-audit/VALIDATION.json +0 -12
- package/.agents/core/skills/redesign-audit/skill.yaml +0 -12
- package/.agents/core/skills/soft-design/SKILL.md +0 -108
- package/.agents/core/skills/soft-design/VALIDATION.json +0 -12
- package/.agents/core/skills/soft-design/skill.yaml +0 -12
- package/.agents/core/skills/state-management/EXAMPLES.md +0 -56
- package/.agents/core/skills/state-management/SKILL.md +0 -48
- package/.agents/core/skills/state-management/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/state-management/VALIDATION.json +0 -11
- package/.agents/core/skills/state-management/skill.yaml +0 -28
- package/.agents/core/skills/subagent-orchestrator/SKILL.md +0 -117
- package/.agents/core/skills/subagent-orchestrator/VALIDATION.json +0 -12
- package/.agents/core/skills/subagent-orchestrator/skill.yaml +0 -12
- package/.agents/core/skills/system-design/EXAMPLES.md +0 -75
- package/.agents/core/skills/system-design/SKILL.md +0 -419
- package/.agents/core/skills/system-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/system-design/VALIDATION.json +0 -12
- package/.agents/core/skills/system-design/skill.yaml +0 -20
- package/.agents/core/skills/system-design/system-design.md +0 -112
- package/.agents/core/skills/testing/EXAMPLES.md +0 -71
- package/.agents/core/skills/testing/SKILL.md +0 -70
- package/.agents/core/skills/testing/TROUBLESHOOTING.md +0 -18
- package/.agents/core/skills/testing/VALIDATION.json +0 -11
- package/.agents/core/skills/testing/skill.yaml +0 -32
- package/.agents/core/skills/typescript/EXAMPLES.md +0 -64
- package/.agents/core/skills/typescript/SKILL.md +0 -112
- package/.agents/core/skills/typescript/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/typescript/VALIDATION.json +0 -12
- package/.agents/core/skills/typescript/skill.yaml +0 -17
- package/.agents/core/skills/typescript/typescript.md +0 -71
- package/.agents/core/skills/ui-design/EXAMPLES.md +0 -21
- package/.agents/core/skills/ui-design/SKILL.md +0 -124
- package/.agents/core/skills/ui-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-design/skill.yaml +0 -17
- package/.agents/core/skills/ui-design/ui.md +0 -88
- package/.agents/core/skills/ui-ux-pro/EXAMPLES.md +0 -62
- package/.agents/core/skills/ui-ux-pro/SKILL.md +0 -375
- package/.agents/core/skills/ui-ux-pro/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ui-ux-pro/VALIDATION.json +0 -12
- package/.agents/core/skills/ui-ux-pro/skill.yaml +0 -19
- package/.agents/core/skills/ux-design/EXAMPLES.md +0 -36
- package/.agents/core/skills/ux-design/SKILL.md +0 -116
- package/.agents/core/skills/ux-design/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/ux-design/VALIDATION.json +0 -12
- package/.agents/core/skills/ux-design/skill.yaml +0 -17
- package/.agents/core/skills/ux-design/ux.md +0 -80
- package/.agents/core/skills/vercel-optimize/SKILL.md +0 -83
- package/.agents/core/skills/vercel-optimize/VALIDATION.json +0 -12
- package/.agents/core/skills/vercel-optimize/scripts/collect-signals.mjs +0 -131
- package/.agents/core/skills/vercel-optimize/scripts/gate-investigations.mjs +0 -142
- package/.agents/core/skills/vercel-optimize/scripts/merge-signals.mjs +0 -143
- package/.agents/core/skills/vercel-optimize/scripts/scan-codebase.mjs +0 -174
- package/.agents/core/skills/vercel-optimize/skill.yaml +0 -18
- package/.agents/core/skills/web-accessibility/EXAMPLES.md +0 -39
- package/.agents/core/skills/web-accessibility/SKILL.md +0 -170
- package/.agents/core/skills/web-accessibility/TROUBLESHOOTING.md +0 -19
- package/.agents/core/skills/web-accessibility/VALIDATION.json +0 -12
- package/.agents/core/skills/web-accessibility/accessibility.md +0 -63
- package/.agents/core/skills/web-accessibility/skill.yaml +0 -17
- package/.agents/generated/claude/skills/adapters/SKILL.md +0 -126
- package/.agents/generated/claude/skills/architecture-diagrams/SKILL.md +0 -101
- package/.agents/generated/claude/skills/brutalist-design/SKILL.md +0 -145
- package/.agents/generated/claude/skills/database/SKILL.md +0 -191
- package/.agents/generated/claude/skills/ddd/SKILL.md +0 -305
- package/.agents/generated/claude/skills/decisions/SKILL.md +0 -134
- package/.agents/generated/claude/skills/docker/SKILL.md +0 -135
- package/.agents/generated/claude/skills/fastapi/SKILL.md +0 -200
- package/.agents/generated/claude/skills/generators/SKILL.md +0 -133
- package/.agents/generated/claude/skills/graphify/SKILL.md +0 -198
- package/.agents/generated/claude/skills/impeccable-design/SKILL.md +0 -241
- package/.agents/generated/claude/skills/interview-me/SKILL.md +0 -90
- package/.agents/generated/claude/skills/microservices/SKILL.md +0 -218
- package/.agents/generated/claude/skills/minimalist-design/SKILL.md +0 -108
- package/.agents/generated/claude/skills/nestjs/SKILL.md +0 -195
- package/.agents/generated/claude/skills/nextjs/SKILL.md +0 -219
- package/.agents/generated/claude/skills/node/SKILL.md +0 -224
- package/.agents/generated/claude/skills/performance/SKILL.md +0 -121
- package/.agents/generated/claude/skills/react/SKILL.md +0 -227
- package/.agents/generated/claude/skills/react-best-practices/SKILL.md +0 -146
- package/.agents/generated/claude/skills/redesign-audit/SKILL.md +0 -112
- package/.agents/generated/claude/skills/soft-design/SKILL.md +0 -103
- package/.agents/generated/claude/skills/state-management/SKILL.md +0 -120
- package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +0 -110
- package/.agents/generated/claude/skills/system-design/SKILL.md +0 -507
- package/.agents/generated/claude/skills/testing/SKILL.md +0 -157
- package/.agents/generated/claude/skills/typescript/SKILL.md +0 -192
- package/.agents/generated/claude/skills/ui-design/SKILL.md +0 -161
- package/.agents/generated/claude/skills/ui-ux-pro/SKILL.md +0 -451
- package/.agents/generated/claude/skills/ux-design/SKILL.md +0 -168
- package/.agents/generated/claude/skills/vercel-optimize/SKILL.md +0 -76
- package/.agents/generated/claude/skills/web-accessibility/SKILL.md +0 -225
- package/.agents/generated/gemini/skills/adapters/SKILL.md +0 -135
- package/.agents/generated/gemini/skills/architecture-diagrams/SKILL.md +0 -107
- package/.agents/generated/gemini/skills/brutalist-design/SKILL.md +0 -151
- package/.agents/generated/gemini/skills/database/SKILL.md +0 -200
- package/.agents/generated/gemini/skills/ddd/SKILL.md +0 -314
- package/.agents/generated/gemini/skills/decisions/SKILL.md +0 -143
- package/.agents/generated/gemini/skills/docker/SKILL.md +0 -144
- package/.agents/generated/gemini/skills/fastapi/SKILL.md +0 -209
- package/.agents/generated/gemini/skills/generators/SKILL.md +0 -142
- package/.agents/generated/gemini/skills/graphify/SKILL.md +0 -205
- package/.agents/generated/gemini/skills/impeccable-design/SKILL.md +0 -250
- package/.agents/generated/gemini/skills/interview-me/SKILL.md +0 -96
- package/.agents/generated/gemini/skills/microservices/SKILL.md +0 -227
- package/.agents/generated/gemini/skills/minimalist-design/SKILL.md +0 -114
- package/.agents/generated/gemini/skills/nestjs/SKILL.md +0 -204
- package/.agents/generated/gemini/skills/nextjs/SKILL.md +0 -298
- package/.agents/generated/gemini/skills/node/SKILL.md +0 -323
- package/.agents/generated/gemini/skills/performance/SKILL.md +0 -185
- package/.agents/generated/gemini/skills/react/SKILL.md +0 -332
- package/.agents/generated/gemini/skills/react-best-practices/SKILL.md +0 -152
- package/.agents/generated/gemini/skills/redesign-audit/SKILL.md +0 -118
- package/.agents/generated/gemini/skills/soft-design/SKILL.md +0 -109
- package/.agents/generated/gemini/skills/state-management/SKILL.md +0 -129
- package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +0 -116
- package/.agents/generated/gemini/skills/system-design/SKILL.md +0 -631
- package/.agents/generated/gemini/skills/testing/SKILL.md +0 -166
- package/.agents/generated/gemini/skills/typescript/SKILL.md +0 -275
- package/.agents/generated/gemini/skills/ui-design/SKILL.md +0 -170
- package/.agents/generated/gemini/skills/ui-ux-pro/SKILL.md +0 -460
- package/.agents/generated/gemini/skills/ux-design/SKILL.md +0 -177
- package/.agents/generated/gemini/skills/vercel-optimize/SKILL.md +0 -82
- package/.agents/generated/gemini/skills/web-accessibility/SKILL.md +0 -300
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/event-store.js
|
|
3
|
+
* ContextOS — Event-Sourced Durable Event Store
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 17.3 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Monotonically incrementing revision log with SHA-256 event checksums
|
|
7
|
+
* - Periodic snapshots with replay recovery
|
|
8
|
+
* - Torn tail detection and automatic quarantine (zero lost valid snapshots)
|
|
9
|
+
* - Atomic event append with collision prevention
|
|
10
|
+
* - Zero install-time dependencies (pure Node.js crypto & fs)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
function sha256(str) {
|
|
20
|
+
return crypto.createHash('sha256').update(str || '').digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class DurableEventStore {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options
|
|
26
|
+
* @param {string} options.baseDir - Base state directory (.agents/.contextos/state)
|
|
27
|
+
* @param {number} [options.snapshotInterval=50] - Events between snapshots
|
|
28
|
+
*/
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
if (!options.baseDir) {
|
|
31
|
+
throw new Error('DurableEventStore requires baseDir');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.baseDir = path.resolve(options.baseDir);
|
|
35
|
+
this.eventsDir = path.join(this.baseDir, 'events');
|
|
36
|
+
this.snapshotsDir = path.join(this.baseDir, 'snapshots');
|
|
37
|
+
this.quarantineDir = path.join(this.baseDir, 'quarantine');
|
|
38
|
+
this.snapshotInterval = Math.max(5, options.snapshotInterval || 50);
|
|
39
|
+
|
|
40
|
+
fs.mkdirSync(this.eventsDir, { recursive: true });
|
|
41
|
+
fs.mkdirSync(this.snapshotsDir, { recursive: true });
|
|
42
|
+
fs.mkdirSync(this.quarantineDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Appends an immutable event to the log.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} streamId - Identifier of the entity/thread
|
|
49
|
+
* @param {Object} eventData - Payload data
|
|
50
|
+
* @param {number} expectedRevision - Optimistic concurrency revision check
|
|
51
|
+
* @returns {Object} Stored event record
|
|
52
|
+
*/
|
|
53
|
+
append(streamId, eventData = {}, expectedRevision = null) {
|
|
54
|
+
const streamDir = path.join(this.eventsDir, streamId);
|
|
55
|
+
fs.mkdirSync(streamDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
// Determine next monotonic revision
|
|
58
|
+
const existingEvents = this._listEventFiles(streamId);
|
|
59
|
+
const lastRevision = existingEvents.length > 0 ? existingEvents[existingEvents.length - 1].revision : 0;
|
|
60
|
+
|
|
61
|
+
if (typeof expectedRevision === 'number' && expectedRevision !== lastRevision) {
|
|
62
|
+
const err = new Error(
|
|
63
|
+
`Optimistic concurrency conflict in stream "${streamId}": expected revision ${expectedRevision}, last revision is ${lastRevision}`
|
|
64
|
+
);
|
|
65
|
+
err.code = 'CTX_EVENT_CONCURRENCY_CONFLICT';
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nextRevision = lastRevision + 1;
|
|
70
|
+
const eventId = crypto.randomUUID();
|
|
71
|
+
const timestamp = Date.now();
|
|
72
|
+
|
|
73
|
+
const record = {
|
|
74
|
+
streamId,
|
|
75
|
+
eventId,
|
|
76
|
+
revision: nextRevision,
|
|
77
|
+
timestamp,
|
|
78
|
+
data: eventData,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const checksum = sha256(JSON.stringify(record));
|
|
82
|
+
const fullEnvelope = {
|
|
83
|
+
...record,
|
|
84
|
+
checksum,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const revPadded = String(nextRevision).padStart(6, '0');
|
|
88
|
+
const filename = `${revPadded}-${eventId}.json`;
|
|
89
|
+
const targetFile = path.join(streamDir, filename);
|
|
90
|
+
const tempFile = `${targetFile}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
fs.writeFileSync(tempFile, JSON.stringify(fullEnvelope, null, 2), 'utf8');
|
|
94
|
+
fs.renameSync(tempFile, targetFile);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Object.freeze(fullEnvelope);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a snapshot of the reduced state up to a given revision.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} streamId
|
|
107
|
+
* @param {Object} state
|
|
108
|
+
* @param {number} revision
|
|
109
|
+
* @returns {Object} Snapshot record
|
|
110
|
+
*/
|
|
111
|
+
createSnapshot(streamId, state, revision) {
|
|
112
|
+
const streamSnapDir = path.join(this.snapshotsDir, streamId);
|
|
113
|
+
fs.mkdirSync(streamSnapDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
const revPadded = String(revision).padStart(6, '0');
|
|
116
|
+
const filename = `snapshot-${revPadded}.json`;
|
|
117
|
+
const targetFile = path.join(streamSnapDir, filename);
|
|
118
|
+
const tempFile = `${targetFile}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
119
|
+
|
|
120
|
+
const snapshot = {
|
|
121
|
+
streamId,
|
|
122
|
+
revision,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
state,
|
|
125
|
+
checksum: sha256(JSON.stringify(state)),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
fs.writeFileSync(tempFile, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
130
|
+
fs.renameSync(tempFile, targetFile);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Object.freeze(snapshot);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reads latest valid snapshot for stream if available.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} streamId
|
|
143
|
+
* @returns {Object|null}
|
|
144
|
+
*/
|
|
145
|
+
getLatestSnapshot(streamId) {
|
|
146
|
+
const streamSnapDir = path.join(this.snapshotsDir, streamId);
|
|
147
|
+
if (!fs.existsSync(streamSnapDir)) return null;
|
|
148
|
+
|
|
149
|
+
const files = fs.readdirSync(streamSnapDir)
|
|
150
|
+
.filter(f => f.startsWith('snapshot-') && f.endsWith('.json'))
|
|
151
|
+
.sort();
|
|
152
|
+
|
|
153
|
+
if (files.length === 0) return null;
|
|
154
|
+
|
|
155
|
+
const latestFile = files[files.length - 1];
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(path.join(streamSnapDir, latestFile), 'utf8');
|
|
158
|
+
const snap = JSON.parse(content);
|
|
159
|
+
// Check integrity
|
|
160
|
+
if (snap.checksum && snap.checksum !== sha256(JSON.stringify(snap.state))) {
|
|
161
|
+
// Corrupted snapshot -> quarantine
|
|
162
|
+
this._quarantineFile(path.join(streamSnapDir, latestFile), 'corrupted_snapshot_checksum');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return snap;
|
|
166
|
+
} catch {
|
|
167
|
+
this._quarantineFile(path.join(streamSnapDir, latestFile), 'unparseable_snapshot');
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Reads all valid events in stream.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} streamId
|
|
176
|
+
* @returns {Array<Object>}
|
|
177
|
+
*/
|
|
178
|
+
readStream(streamId) {
|
|
179
|
+
const eventFiles = this._listEventFiles(streamId);
|
|
180
|
+
const events = [];
|
|
181
|
+
for (const item of eventFiles) {
|
|
182
|
+
const evt = this._readAndVerifyEvent(item.filePath);
|
|
183
|
+
if (evt) {
|
|
184
|
+
events.push(evt);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return events;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Replays events from snapshot or beginning to restore current state.
|
|
192
|
+
* Tolerates and quarantines torn tail events from unexpected crashes.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} streamId
|
|
195
|
+
* @param {Function} reducer - (state, event) => newState
|
|
196
|
+
* @param {Object} [initialState={}]
|
|
197
|
+
* @returns {{ state: Object, revision: number, replayedEvents: number, quarantinedCount: number }}
|
|
198
|
+
*/
|
|
199
|
+
replay(streamId, reducer, initialState = {}) {
|
|
200
|
+
if (typeof reducer !== 'function') {
|
|
201
|
+
throw new Error('replay requires a reducer function');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const snapshot = this.getLatestSnapshot(streamId);
|
|
205
|
+
let state = snapshot ? JSON.parse(JSON.stringify(snapshot.state)) : JSON.parse(JSON.stringify(initialState));
|
|
206
|
+
let lastRevision = snapshot ? snapshot.revision : 0;
|
|
207
|
+
|
|
208
|
+
const eventFiles = this._listEventFiles(streamId);
|
|
209
|
+
let replayedEvents = 0;
|
|
210
|
+
let quarantinedCount = 0;
|
|
211
|
+
|
|
212
|
+
for (const item of eventFiles) {
|
|
213
|
+
if (item.revision <= lastRevision) continue;
|
|
214
|
+
|
|
215
|
+
const eventData = this._readAndVerifyEvent(item.filePath);
|
|
216
|
+
if (!eventData) {
|
|
217
|
+
// Torn tail detected at end of stream! Quarantine it to recover.
|
|
218
|
+
this._quarantineFile(item.filePath, 'torn_tail_corrupted_event');
|
|
219
|
+
quarantinedCount++;
|
|
220
|
+
break; // Stop replaying past torn tail
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
state = reducer(state, eventData);
|
|
224
|
+
lastRevision = eventData.revision;
|
|
225
|
+
replayedEvents++;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
state: Object.freeze(state),
|
|
230
|
+
revision: lastRevision,
|
|
231
|
+
replayedEvents,
|
|
232
|
+
quarantinedCount,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_listEventFiles(streamId) {
|
|
237
|
+
const streamDir = path.join(this.eventsDir, streamId);
|
|
238
|
+
if (!fs.existsSync(streamDir)) return [];
|
|
239
|
+
|
|
240
|
+
const files = fs.readdirSync(streamDir)
|
|
241
|
+
.filter(f => /^\d{6}-[a-zA-Z0-9-]+\.json$/.test(f))
|
|
242
|
+
.sort();
|
|
243
|
+
|
|
244
|
+
return files.map(f => {
|
|
245
|
+
const rev = parseInt(f.slice(0, 6), 10);
|
|
246
|
+
return {
|
|
247
|
+
filename: f,
|
|
248
|
+
revision: rev,
|
|
249
|
+
filePath: path.join(streamDir, f),
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_readAndVerifyEvent(filePath) {
|
|
255
|
+
try {
|
|
256
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
257
|
+
const envelope = JSON.parse(content);
|
|
258
|
+
|
|
259
|
+
if (!envelope.revision || !envelope.checksum) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { checksum, ...rest } = envelope;
|
|
264
|
+
const expectedChecksum = sha256(JSON.stringify(rest));
|
|
265
|
+
if (checksum !== expectedChecksum) {
|
|
266
|
+
return null; // Checksum mismatch
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return envelope;
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_quarantineFile(filePath, reason) {
|
|
276
|
+
try {
|
|
277
|
+
const filename = path.basename(filePath);
|
|
278
|
+
const target = path.join(this.quarantineDir, `${Date.now()}-${reason}-${filename}`);
|
|
279
|
+
fs.renameSync(filePath, target);
|
|
280
|
+
} catch {
|
|
281
|
+
// ignore
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
DurableEventStore,
|
|
288
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .agents/runtime/idempotency.js
|
|
3
|
+
* ContextOS — Runtime Idempotency Engine
|
|
4
|
+
*
|
|
5
|
+
* Implements Section 17.5 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
|
|
6
|
+
* - Idempotency key registration and deduplication
|
|
7
|
+
* - Safe replay: returns existing job/worktree without spawning duplicate branches
|
|
8
|
+
* - Conflict detection: raises error if payload differs for the same idempotency key
|
|
9
|
+
* - Atomic persistence to disk
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
function sha256(str) {
|
|
19
|
+
return crypto.createHash('sha256').update(str || '').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class IdempotencyRegistry {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {string} options.baseDir - Base directory (.agents/.contextos)
|
|
26
|
+
*/
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.rootDir = path.resolve(options.baseDir || process.cwd());
|
|
29
|
+
this.registryDir = path.join(this.rootDir, '.agents', '.contextos', 'idempotency');
|
|
30
|
+
fs.mkdirSync(this.registryDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getKeyPath(idempotencyKey) {
|
|
34
|
+
const safeKey = crypto.createHash('sha256').update(idempotencyKey).digest('hex');
|
|
35
|
+
return path.join(this.registryDir, `${safeKey}.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hashes request payload deterministically.
|
|
40
|
+
*/
|
|
41
|
+
hashPayload(payload) {
|
|
42
|
+
if (typeof payload === 'string') return sha256(payload);
|
|
43
|
+
return sha256(JSON.stringify(payload || {}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if an idempotency key already exists.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} idempotencyKey
|
|
50
|
+
* @returns {Object|null}
|
|
51
|
+
*/
|
|
52
|
+
get(idempotencyKey) {
|
|
53
|
+
if (!idempotencyKey) return null;
|
|
54
|
+
const keyPath = this._getKeyPath(idempotencyKey);
|
|
55
|
+
if (!fs.existsSync(keyPath)) return null;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = fs.readFileSync(keyPath, 'utf8');
|
|
59
|
+
return JSON.parse(content);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes an operation with idempotency protection.
|
|
67
|
+
* If key was previously executed with identical payload, returns cached result.
|
|
68
|
+
* If payload differs, throws CTX_IDEMPOTENCY_CONFLICT.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} idempotencyKey
|
|
71
|
+
* @param {Object|string} payload
|
|
72
|
+
* @param {Function} factoryFn - Async function returning { jobId, threadId, branchName, worktreePath }
|
|
73
|
+
* @returns {Promise<{ isExisting: boolean, record: Object }>}
|
|
74
|
+
*/
|
|
75
|
+
async execute(idempotencyKey, payload, factoryFn) {
|
|
76
|
+
if (!idempotencyKey) {
|
|
77
|
+
// No idempotency key -> execute directly without caching
|
|
78
|
+
const result = await factoryFn();
|
|
79
|
+
return { isExisting: false, record: result };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payloadHash = this.hashPayload(payload);
|
|
83
|
+
const existing = this.get(idempotencyKey);
|
|
84
|
+
|
|
85
|
+
if (existing) {
|
|
86
|
+
if (existing.payloadHash !== payloadHash) {
|
|
87
|
+
const err = new Error(
|
|
88
|
+
`Idempotency conflict for key "${idempotencyKey}": payload does not match previously submitted request.`
|
|
89
|
+
);
|
|
90
|
+
err.code = 'CTX_IDEMPOTENCY_CONFLICT';
|
|
91
|
+
err.idempotencyKey = idempotencyKey;
|
|
92
|
+
err.existingJobId = existing.jobId;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Existing request matched identically -> return existing job without creating duplicate
|
|
97
|
+
return { isExisting: true, record: existing };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Execute operation
|
|
101
|
+
const result = await factoryFn();
|
|
102
|
+
|
|
103
|
+
const record = {
|
|
104
|
+
idempotencyKey,
|
|
105
|
+
payloadHash,
|
|
106
|
+
jobId: result.jobId || result.id || `job-${Date.now()}`,
|
|
107
|
+
threadId: result.threadId || result.id || null,
|
|
108
|
+
branchName: result.branchName || null,
|
|
109
|
+
worktreePath: result.worktreePath || null,
|
|
110
|
+
createdAt: Date.now(),
|
|
111
|
+
metadata: result,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const keyPath = this._getKeyPath(idempotencyKey);
|
|
115
|
+
const tempPath = `${keyPath}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
fs.writeFileSync(tempPath, JSON.stringify(record, null, 2), 'utf8');
|
|
118
|
+
fs.renameSync(tempPath, keyPath);
|
|
119
|
+
} catch {
|
|
120
|
+
try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { isExisting: false, record };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
IdempotencyRegistry,
|
|
129
|
+
};
|