pi-crew 0.1.49 → 0.2.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/CHANGELOG.md +74 -1
- package/README.md +176 -781
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +70 -11
- package/agents/writer.md +11 -11
- package/docs/actions-reference.md +595 -0
- package/docs/commands-reference.md +347 -0
- package/docs/runtime-flow.md +148 -148
- package/index.ts +6 -6
- package/package.json +99 -99
- package/skills/async-worker-recovery/SKILL.md +42 -42
- package/skills/context-artifact-hygiene/SKILL.md +52 -52
- package/skills/delegation-patterns/SKILL.md +54 -54
- package/skills/mailbox-interactive/SKILL.md +40 -40
- package/skills/model-routing-context/SKILL.md +39 -39
- package/skills/multi-perspective-review/SKILL.md +58 -58
- package/skills/observability-reliability/SKILL.md +41 -41
- package/skills/orchestration/SKILL.md +157 -157
- package/skills/ownership-session-security/SKILL.md +41 -41
- package/skills/pi-extension-lifecycle/SKILL.md +39 -39
- package/skills/requirements-to-task-packet/SKILL.md +63 -63
- package/skills/resource-discovery-config/SKILL.md +41 -41
- package/skills/runtime-state-reader/SKILL.md +44 -44
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
- package/skills/state-mutation-locking/SKILL.md +42 -42
- package/skills/systematic-debugging/SKILL.md +67 -67
- package/skills/ui-render-performance/SKILL.md +39 -39
- package/skills/verification-before-done/SKILL.md +57 -57
- package/skills/worktree-isolation/SKILL.md +39 -39
- package/src/adapters/claude-adapter.ts +25 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +137 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/agent-search.ts +98 -98
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +14 -1
- package/src/config/defaults.ts +5 -5
- package/src/config/drift-detector.ts +211 -0
- package/src/config/markers.ts +327 -0
- package/src/config/resilient-parser.ts +108 -0
- package/src/config/suggestions.ts +74 -0
- package/src/extension/cross-extension-rpc.ts +103 -82
- package/src/extension/project-init.ts +36 -4
- package/src/extension/register.ts +67 -22
- package/src/extension/registration/commands.ts +77 -8
- package/src/extension/registration/subagent-tools.ts +10 -1
- package/src/extension/registration/team-tool.ts +10 -1
- package/src/extension/registration/viewers.ts +48 -34
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +26 -12
- package/src/extension/run-import.ts +25 -1
- package/src/extension/run-index.ts +5 -1
- package/src/extension/run-maintenance.ts +142 -68
- package/src/extension/team-manager-command.ts +10 -1
- package/src/extension/team-tool/context.ts +1 -1
- package/src/extension/team-tool/doctor.ts +28 -3
- package/src/extension/team-tool/handle-settings.ts +195 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/intent-policy.ts +42 -42
- package/src/extension/team-tool/lifecycle-actions.ts +27 -8
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +12 -1
- package/src/extension/team-tool.ts +14 -3
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +92 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -0
- package/src/runtime/background-runner.ts +11 -1
- package/src/runtime/cancellation-token.ts +89 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi.ts +7 -2
- package/src/runtime/compaction-summary.ts +271 -0
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/concurrency.ts +3 -1
- package/src/runtime/crash-recovery.ts +33 -0
- package/src/runtime/delta-conflict.ts +360 -0
- package/src/runtime/diagnostic-export.ts +3 -1
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/event-stream-bridge.ts +3 -1
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/iteration-hooks.ts +262 -0
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-extension-bridge.ts +150 -150
- package/src/runtime/live-irc.ts +92 -92
- package/src/runtime/live-session-health.ts +100 -100
- package/src/runtime/loop-gates.ts +129 -0
- package/src/runtime/metric-parser.ts +40 -0
- package/src/runtime/notebook-helpers.ts +90 -90
- package/src/runtime/orphan-sentinel.ts +7 -7
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/pi-args.ts +38 -2
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +74 -6
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +122 -0
- package/src/runtime/process-status.ts +14 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/prose-compressor.ts +164 -164
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/result-extractor.ts +121 -121
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/sensitive-paths.ts +3 -3
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stream-preview.ts +177 -177
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph.ts +207 -0
- package/src/runtime/task-quality.ts +207 -0
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +7 -1
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +1 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +126 -7
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -0
- package/src/runtime/workspace-tree.ts +298 -298
- package/src/schema/config-schema.ts +12 -0
- package/src/schema/validation-types.ts +148 -0
- package/src/skills/skill-templates.ts +374 -0
- package/src/state/active-run-registry.ts +35 -11
- package/src/state/atomic-write.ts +33 -26
- package/src/state/contracts.ts +1 -0
- package/src/state/event-reconstructor.ts +217 -0
- package/src/state/locks.ts +2 -11
- package/src/state/mailbox.ts +4 -3
- package/src/state/state-store.ts +32 -14
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +9 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +9 -4
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +11 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +6 -0
- package/src/ui/render-coalescer.ts +51 -51
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-action-dispatcher.ts +10 -1
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-entries.ts +258 -258
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sleep.ts +40 -26
- package/src/utils/task-name-generator.ts +337 -337
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +11 -3
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +30 -29
- package/workflows/fast-fix.workflow.md +23 -22
- package/workflows/implementation.workflow.md +43 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/docs/refactor-tasks-phase3.md +0 -394
- package/docs/refactor-tasks-phase4.md +0 -564
- package/docs/refactor-tasks-phase5.md +0 -402
- package/docs/refactor-tasks-phase6.md +0 -662
- package/docs/refactor-tasks.md +0 -1484
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
- package/docs/research/AUDIT_OH_MY_PI.md +0 -261
- package/docs/research/AUDIT_PI_CREW.md +0 -457
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
- package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
- package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
- package/docs/research-awesome-agent-skills-distillation.md +0 -100
- package/docs/research-extension-examples.md +0 -297
- package/docs/research-extension-system.md +0 -324
- package/docs/research-oh-my-pi-distillation.md +0 -369
- package/docs/research-optimization-plan.md +0 -548
- package/docs/research-phase10-distillation.md +0 -199
- package/docs/research-phase11-distillation.md +0 -201
- package/docs/research-phase8-operator-experience-plan.md +0 -819
- package/docs/research-phase9-observability-reliability-plan.md +0 -1190
- package/docs/research-pi-coding-agent.md +0 -357
- package/docs/research-source-pi-crew-reference.md +0 -174
- package/docs/research-ui-optimization-plan.md +0 -480
- package/docs/source-runtime-refactor-map.md +0 -107
- package/src/utils/atomic-write.ts +0 -33
package/src/utils/scan-cache.ts
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface ScanEntry {
|
|
5
|
-
/** Unique key for this entry (e.g., runId, artifactPath) */
|
|
6
|
-
key: string;
|
|
7
|
-
/** Filesystem path */
|
|
8
|
-
path: string;
|
|
9
|
-
/** Raw content (parsed JSON or text) */
|
|
10
|
-
raw: unknown;
|
|
11
|
-
/** File modification time */
|
|
12
|
-
mtimeMs: number;
|
|
13
|
-
/** File size in bytes */
|
|
14
|
-
sizeBytes: number;
|
|
15
|
-
/** When this entry was loaded */
|
|
16
|
-
loadedAtMs: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ScanCacheOptions {
|
|
20
|
-
/** TTL in ms for cached entries. Default 1000. */
|
|
21
|
-
ttlMs?: number;
|
|
22
|
-
/** Maximum number of entries. Default 100. */
|
|
23
|
-
maxEntries?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface CacheBucket {
|
|
27
|
-
entries: Map<string, ScanEntry>;
|
|
28
|
-
expireAtMs: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
|
|
33
|
-
* Provides deterministic sort order and invalidation on mutation.
|
|
34
|
-
*/
|
|
35
|
-
export class SharedScanCache {
|
|
36
|
-
#buckets = new Map<string, CacheBucket>();
|
|
37
|
-
#ttlMs: number;
|
|
38
|
-
#maxEntries: number;
|
|
39
|
-
#now: () => number;
|
|
40
|
-
|
|
41
|
-
constructor(options: ScanCacheOptions = {}) {
|
|
42
|
-
this.#ttlMs = options.ttlMs ?? 1000;
|
|
43
|
-
this.#maxEntries = options.maxEntries ?? 100;
|
|
44
|
-
this.#now = () => Date.now();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
|
|
48
|
-
get(bucket: string, key: string): ScanEntry | undefined {
|
|
49
|
-
const b = this.#buckets.get(bucket);
|
|
50
|
-
if (!b) return undefined;
|
|
51
|
-
if (this.#now() > b.expireAtMs) {
|
|
52
|
-
this.#buckets.delete(bucket);
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
return b.entries.get(key);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Get all entries in a bucket. Returns empty array if expired or missing. */
|
|
59
|
-
list(bucket: string): ScanEntry[] {
|
|
60
|
-
const b = this.#buckets.get(bucket);
|
|
61
|
-
if (!b) return [];
|
|
62
|
-
if (this.#now() > b.expireAtMs) {
|
|
63
|
-
this.#buckets.delete(bucket);
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Set an entry in a bucket. */
|
|
70
|
-
set(bucket: string, entry: ScanEntry): void {
|
|
71
|
-
let b = this.#buckets.get(bucket);
|
|
72
|
-
if (!b || this.#now() > b.expireAtMs) {
|
|
73
|
-
b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
|
|
74
|
-
this.#buckets.set(bucket, b);
|
|
75
|
-
}
|
|
76
|
-
if (b.entries.size >= this.#maxEntries) {
|
|
77
|
-
// Evict oldest entry
|
|
78
|
-
const firstKey = b.entries.keys().next().value;
|
|
79
|
-
if (firstKey !== undefined) b.entries.delete(firstKey);
|
|
80
|
-
}
|
|
81
|
-
b.entries.set(entry.key, entry);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Invalidate a specific key in a bucket. */
|
|
85
|
-
invalidate(bucket: string, key: string): void {
|
|
86
|
-
const b = this.#buckets.get(bucket);
|
|
87
|
-
if (b) b.entries.delete(key);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Invalidate an entire bucket. */
|
|
91
|
-
invalidateBucket(bucket: string): void {
|
|
92
|
-
this.#buckets.delete(bucket);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Invalidate all buckets. */
|
|
96
|
-
clear(): void {
|
|
97
|
-
this.#buckets.clear();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Read a file, parse if JSON, and cache the result. */
|
|
101
|
-
readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
|
|
102
|
-
try {
|
|
103
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
104
|
-
const stat = fs.statSync(filePath);
|
|
105
|
-
const cached = this.get(bucket, key);
|
|
106
|
-
if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
|
|
107
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
108
|
-
const raw = parseJson ? JSON.parse(content) : content;
|
|
109
|
-
const entry: ScanEntry = { key, path: filePath, raw, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, loadedAtMs: this.#now() };
|
|
110
|
-
this.set(bucket, entry);
|
|
111
|
-
return entry;
|
|
112
|
-
} catch {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Read a directory and cache entries for each file. */
|
|
118
|
-
scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
|
|
119
|
-
try {
|
|
120
|
-
if (!fs.existsSync(dirPath)) return [];
|
|
121
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
122
|
-
const results: ScanEntry[] = [];
|
|
123
|
-
for (const entry of entries) {
|
|
124
|
-
if (!entry.isFile()) continue;
|
|
125
|
-
const filePath = path.join(dirPath, entry.name);
|
|
126
|
-
const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
|
|
127
|
-
if (cached) results.push(cached);
|
|
128
|
-
}
|
|
129
|
-
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
130
|
-
} catch {
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Global shared scan cache instance. */
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ScanEntry {
|
|
5
|
+
/** Unique key for this entry (e.g., runId, artifactPath) */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Filesystem path */
|
|
8
|
+
path: string;
|
|
9
|
+
/** Raw content (parsed JSON or text) */
|
|
10
|
+
raw: unknown;
|
|
11
|
+
/** File modification time */
|
|
12
|
+
mtimeMs: number;
|
|
13
|
+
/** File size in bytes */
|
|
14
|
+
sizeBytes: number;
|
|
15
|
+
/** When this entry was loaded */
|
|
16
|
+
loadedAtMs: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScanCacheOptions {
|
|
20
|
+
/** TTL in ms for cached entries. Default 1000. */
|
|
21
|
+
ttlMs?: number;
|
|
22
|
+
/** Maximum number of entries. Default 100. */
|
|
23
|
+
maxEntries?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CacheBucket {
|
|
27
|
+
entries: Map<string, ScanEntry>;
|
|
28
|
+
expireAtMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared raw scan-entry cache for runs, artifacts, mailbox, transcripts.
|
|
33
|
+
* Provides deterministic sort order and invalidation on mutation.
|
|
34
|
+
*/
|
|
35
|
+
export class SharedScanCache {
|
|
36
|
+
#buckets = new Map<string, CacheBucket>();
|
|
37
|
+
#ttlMs: number;
|
|
38
|
+
#maxEntries: number;
|
|
39
|
+
#now: () => number;
|
|
40
|
+
|
|
41
|
+
constructor(options: ScanCacheOptions = {}) {
|
|
42
|
+
this.#ttlMs = options.ttlMs ?? 1000;
|
|
43
|
+
this.#maxEntries = options.maxEntries ?? 100;
|
|
44
|
+
this.#now = () => Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Get a cached entry by bucket and key. Returns undefined if not cached or expired. */
|
|
48
|
+
get(bucket: string, key: string): ScanEntry | undefined {
|
|
49
|
+
const b = this.#buckets.get(bucket);
|
|
50
|
+
if (!b) return undefined;
|
|
51
|
+
if (this.#now() > b.expireAtMs) {
|
|
52
|
+
this.#buckets.delete(bucket);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return b.entries.get(key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Get all entries in a bucket. Returns empty array if expired or missing. */
|
|
59
|
+
list(bucket: string): ScanEntry[] {
|
|
60
|
+
const b = this.#buckets.get(bucket);
|
|
61
|
+
if (!b) return [];
|
|
62
|
+
if (this.#now() > b.expireAtMs) {
|
|
63
|
+
this.#buckets.delete(bucket);
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [...b.entries.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Set an entry in a bucket. */
|
|
70
|
+
set(bucket: string, entry: ScanEntry): void {
|
|
71
|
+
let b = this.#buckets.get(bucket);
|
|
72
|
+
if (!b || this.#now() > b.expireAtMs) {
|
|
73
|
+
b = { entries: new Map(), expireAtMs: this.#now() + this.#ttlMs };
|
|
74
|
+
this.#buckets.set(bucket, b);
|
|
75
|
+
}
|
|
76
|
+
if (b.entries.size >= this.#maxEntries) {
|
|
77
|
+
// Evict oldest entry
|
|
78
|
+
const firstKey = b.entries.keys().next().value;
|
|
79
|
+
if (firstKey !== undefined) b.entries.delete(firstKey);
|
|
80
|
+
}
|
|
81
|
+
b.entries.set(entry.key, entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Invalidate a specific key in a bucket. */
|
|
85
|
+
invalidate(bucket: string, key: string): void {
|
|
86
|
+
const b = this.#buckets.get(bucket);
|
|
87
|
+
if (b) b.entries.delete(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Invalidate an entire bucket. */
|
|
91
|
+
invalidateBucket(bucket: string): void {
|
|
92
|
+
this.#buckets.delete(bucket);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Invalidate all buckets. */
|
|
96
|
+
clear(): void {
|
|
97
|
+
this.#buckets.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Read a file, parse if JSON, and cache the result. */
|
|
101
|
+
readAndCache(bucket: string, key: string, filePath: string, parseJson = true): ScanEntry | undefined {
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
104
|
+
const stat = fs.statSync(filePath);
|
|
105
|
+
const cached = this.get(bucket, key);
|
|
106
|
+
if (cached && cached.mtimeMs >= stat.mtimeMs && cached.sizeBytes === stat.size) return cached;
|
|
107
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
108
|
+
const raw = parseJson ? JSON.parse(content) : content;
|
|
109
|
+
const entry: ScanEntry = { key, path: filePath, raw, mtimeMs: stat.mtimeMs, sizeBytes: stat.size, loadedAtMs: this.#now() };
|
|
110
|
+
this.set(bucket, entry);
|
|
111
|
+
return entry;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read a directory and cache entries for each file. */
|
|
118
|
+
scanAndCache(bucket: string, dirPath: string, parseJson = true): ScanEntry[] {
|
|
119
|
+
try {
|
|
120
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
121
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
122
|
+
const results: ScanEntry[] = [];
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (!entry.isFile()) continue;
|
|
125
|
+
const filePath = path.join(dirPath, entry.name);
|
|
126
|
+
const cached = this.readAndCache(bucket, entry.name, filePath, parseJson);
|
|
127
|
+
if (cached) results.push(cached);
|
|
128
|
+
}
|
|
129
|
+
return results.sort((a, b) => a.key.localeCompare(b.key));
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Global shared scan cache instance. */
|
|
137
137
|
export const sharedScanCache = new SharedScanCache();
|
package/src/utils/sleep.ts
CHANGED
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Synchronous sleep using Atomics.wait (non-busy) with low-CPU fallback.
|
|
3
|
+
*
|
|
4
|
+
* WARNING: This blocks the Node.js main thread. Only use in sync I/O paths
|
|
5
|
+
* where blocking is acceptable (lock acquisition, rename retry).
|
|
6
|
+
* NOT safe to call from Pi extension async code paths.
|
|
3
7
|
*/
|
|
4
|
-
export function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
export function sleepSync(ms: number): void {
|
|
9
|
+
try {
|
|
10
|
+
const buffer = new SharedArrayBuffer(4);
|
|
11
|
+
Atomics.wait(new Int32Array(buffer), 0, 0, ms);
|
|
12
|
+
} catch {
|
|
13
|
+
// Fallback for environments without Atomics.wait (e.g. Windows).
|
|
14
|
+
// On Unix, try spawning the `sleep` command to avoid CPU busy-wait.
|
|
15
|
+
if (process.platform !== "win32" && ms >= 10) {
|
|
16
|
+
try {
|
|
17
|
+
const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
|
|
18
|
+
execFileSync("sleep", [(ms / 1000).toFixed(3)], { timeout: ms + 1000, stdio: "pipe" });
|
|
19
|
+
return;
|
|
20
|
+
} catch {
|
|
21
|
+
// sleep command unavailable — fall through to busy-wait
|
|
22
|
+
}
|
|
9
23
|
}
|
|
24
|
+
// Last resort: busy-wait. This burns CPU but is the only guaranteed
|
|
25
|
+
// synchronous delay on Windows without SharedArrayBuffer.
|
|
26
|
+
const deadline = Date.now() + ms;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
// Busy-wait fallback for environments without Atomics.wait.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
10
32
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (settled) return;
|
|
24
|
-
settled = true;
|
|
25
|
-
clearTimeout(timeout);
|
|
26
|
-
cleanup();
|
|
27
|
-
reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
signal?.addEventListener("abort", onAbort);
|
|
33
|
+
/**
|
|
34
|
+
* Async sleep with optional AbortSignal support.
|
|
35
|
+
* Rejects immediately if the signal is already aborted, or when aborted during wait.
|
|
36
|
+
*/
|
|
37
|
+
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
38
|
+
if (signal?.aborted) return Promise.reject(new Error("aborted"));
|
|
39
|
+
return new Promise<void>((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(resolve, ms);
|
|
41
|
+
signal?.addEventListener("abort", () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
reject(new Error("aborted"));
|
|
44
|
+
}, { once: true });
|
|
31
45
|
});
|
|
32
46
|
}
|