@animalabs/agent-framework 0.21.1 → 0.22.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 +43 -0
- package/dist/src/agent.d.ts +17 -4
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +102 -42
- package/dist/src/agent.js.map +1 -1
- package/dist/src/code-execution/py-runner.d.ts +22 -1
- package/dist/src/code-execution/py-runner.d.ts.map +1 -1
- package/dist/src/code-execution/py-runner.js +64 -9
- package/dist/src/code-execution/py-runner.js.map +1 -1
- package/dist/src/code-execution/runtime-py.d.ts +4 -0
- package/dist/src/code-execution/runtime-py.d.ts.map +1 -1
- package/dist/src/code-execution/runtime-py.js +215 -38
- package/dist/src/code-execution/runtime-py.js.map +1 -1
- package/dist/src/framework.d.ts +250 -39
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1602 -371
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +21 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +40 -7
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +26 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts +28 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +17 -0
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/feature-set-manager.d.ts +11 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
- package/dist/src/mcpl/feature-set-manager.js +31 -7
- package/dist/src/mcpl/feature-set-manager.js.map +1 -1
- package/dist/src/mcpl/index.d.ts +2 -0
- package/dist/src/mcpl/index.d.ts.map +1 -1
- package/dist/src/mcpl/index.js +1 -0
- package/dist/src/mcpl/index.js.map +1 -1
- package/dist/src/mcpl/prose-stream-router.d.ts +27 -6
- package/dist/src/mcpl/prose-stream-router.d.ts.map +1 -1
- package/dist/src/mcpl/prose-stream-router.js +85 -22
- package/dist/src/mcpl/prose-stream-router.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +32 -4
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js +46 -13
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +5 -3
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +41 -11
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +4 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +45 -8
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/server-registry.d.ts +5 -2
- package/dist/src/mcpl/server-registry.d.ts.map +1 -1
- package/dist/src/mcpl/server-registry.js +25 -7
- package/dist/src/mcpl/server-registry.js.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.d.ts +52 -2
- package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
- package/dist/src/mcpl/tool-lifecycle.js +47 -1
- package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
- package/dist/src/mcpl/tool-pattern-check.d.ts +51 -0
- package/dist/src/mcpl/tool-pattern-check.d.ts.map +1 -0
- package/dist/src/mcpl/tool-pattern-check.js +79 -0
- package/dist/src/mcpl/tool-pattern-check.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +15 -5
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/mcpl/visible-content.d.ts +39 -0
- package/dist/src/mcpl/visible-content.d.ts.map +1 -0
- package/dist/src/mcpl/visible-content.js +54 -0
- package/dist/src/mcpl/visible-content.js.map +1 -0
- package/dist/src/module-registry.d.ts +81 -3
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +257 -16
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/workspace/branch-intent.d.ts +72 -0
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -0
- package/dist/src/modules/workspace/branch-intent.js +83 -0
- package/dist/src/modules/workspace/branch-intent.js.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts +167 -0
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -0
- package/dist/src/modules/workspace/disk-agreement.js +259 -0
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -0
- package/dist/src/modules/workspace/effects.d.ts +84 -0
- package/dist/src/modules/workspace/effects.d.ts.map +1 -0
- package/dist/src/modules/workspace/effects.js +368 -0
- package/dist/src/modules/workspace/effects.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +155 -17
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +1059 -424
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +278 -0
- package/dist/src/modules/workspace/observe.d.ts.map +1 -0
- package/dist/src/modules/workspace/observe.js +805 -0
- package/dist/src/modules/workspace/observe.js.map +1 -0
- package/dist/src/modules/workspace/reconcile.d.ts +224 -0
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -0
- package/dist/src/modules/workspace/reconcile.js +901 -0
- package/dist/src/modules/workspace/reconcile.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +6 -59
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +6 -257
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +77 -10
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/record-journal.d.ts +127 -0
- package/dist/src/record-journal.d.ts.map +1 -0
- package/dist/src/record-journal.js +202 -0
- package/dist/src/record-journal.js.map +1 -0
- package/dist/src/shutdown.d.ts +13 -0
- package/dist/src/shutdown.d.ts.map +1 -0
- package/dist/src/shutdown.js +46 -0
- package/dist/src/shutdown.js.map +1 -0
- package/dist/src/silent-heartbeat.d.ts +74 -0
- package/dist/src/silent-heartbeat.d.ts.map +1 -0
- package/dist/src/silent-heartbeat.js +77 -0
- package/dist/src/silent-heartbeat.js.map +1 -0
- package/dist/src/tool-presentation.d.ts +50 -0
- package/dist/src/tool-presentation.d.ts.map +1 -0
- package/dist/src/tool-presentation.js +235 -0
- package/dist/src/tool-presentation.js.map +1 -0
- package/dist/src/tool-result-guard.d.ts +125 -6
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +310 -35
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tool-result-history.d.ts +13 -0
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +21 -1
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/types/agent.d.ts +18 -2
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +4 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +12 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +14 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +73 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/batched-wake-locus.test.d.ts +2 -0
- package/dist/test/batched-wake-locus.test.d.ts.map +1 -0
- package/dist/test/batched-wake-locus.test.js +166 -0
- package/dist/test/batched-wake-locus.test.js.map +1 -0
- package/dist/test/code-execution.test.js +386 -2
- package/dist/test/code-execution.test.js.map +1 -1
- package/dist/test/component-defaults.test.d.ts +2 -0
- package/dist/test/component-defaults.test.d.ts.map +1 -0
- package/dist/test/component-defaults.test.js +54 -0
- package/dist/test/component-defaults.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +57 -1
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/ephemeral-settle.test.js +3 -0
- package/dist/test/ephemeral-settle.test.js.map +1 -1
- package/dist/test/event-gate.test.js +118 -0
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/explicit-prose-routing.test.js +977 -0
- package/dist/test/explicit-prose-routing.test.js.map +1 -1
- package/dist/test/helpers/pytc-stop-rows.d.ts +11 -0
- package/dist/test/helpers/pytc-stop-rows.d.ts.map +1 -0
- package/dist/test/helpers/pytc-stop-rows.js +143 -0
- package/dist/test/helpers/pytc-stop-rows.js.map +1 -0
- package/dist/test/mcpl-coalescing.test.js +19 -6
- package/dist/test/mcpl-coalescing.test.js.map +1 -1
- package/dist/test/mcpl-config-diagnostics.test.d.ts +2 -0
- package/dist/test/mcpl-config-diagnostics.test.d.ts.map +1 -0
- package/dist/test/mcpl-config-diagnostics.test.js +449 -0
- package/dist/test/mcpl-config-diagnostics.test.js.map +1 -0
- package/dist/test/mcpl-empty-content.test.d.ts +2 -0
- package/dist/test/mcpl-empty-content.test.d.ts.map +1 -0
- package/dist/test/mcpl-empty-content.test.js +368 -0
- package/dist/test/mcpl-empty-content.test.js.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js +106 -1
- package/dist/test/mcpl-tool-lifecycle-emitter.test.js.map +1 -1
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts +2 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.d.ts.map +1 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js +288 -0
- package/dist/test/mcpl-tool-lifecycle-script.test.js.map +1 -0
- package/dist/test/prose-stream-router.test.js +41 -0
- package/dist/test/prose-stream-router.test.js.map +1 -1
- package/dist/test/record-journal.test.d.ts +6 -0
- package/dist/test/record-journal.test.d.ts.map +1 -0
- package/dist/test/record-journal.test.js +269 -0
- package/dist/test/record-journal.test.js.map +1 -0
- package/dist/test/save-recent-image.test.js +102 -0
- package/dist/test/save-recent-image.test.js.map +1 -1
- package/dist/test/shutdown-errors.test.d.ts +2 -0
- package/dist/test/shutdown-errors.test.d.ts.map +1 -0
- package/dist/test/shutdown-errors.test.js +662 -0
- package/dist/test/shutdown-errors.test.js.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts +2 -0
- package/dist/test/silent-heartbeat-rows.test.d.ts.map +1 -0
- package/dist/test/silent-heartbeat-rows.test.js +451 -0
- package/dist/test/silent-heartbeat-rows.test.js.map +1 -0
- package/dist/test/silent-heartbeat.test.js +49 -38
- package/dist/test/silent-heartbeat.test.js.map +1 -1
- package/dist/test/stop-with-work-in-flight.test.d.ts +23 -0
- package/dist/test/stop-with-work-in-flight.test.d.ts.map +1 -0
- package/dist/test/stop-with-work-in-flight.test.js +672 -0
- package/dist/test/stop-with-work-in-flight.test.js.map +1 -0
- package/dist/test/tool-batch-hook.test.d.ts +8 -0
- package/dist/test/tool-batch-hook.test.d.ts.map +1 -0
- package/dist/test/tool-batch-hook.test.js +275 -0
- package/dist/test/tool-batch-hook.test.js.map +1 -0
- package/dist/test/tool-presentation.test.d.ts +2 -0
- package/dist/test/tool-presentation.test.d.ts.map +1 -0
- package/dist/test/tool-presentation.test.js +268 -0
- package/dist/test/tool-presentation.test.js.map +1 -0
- package/dist/test/tool-result-guard.test.js +2330 -18
- package/dist/test/tool-result-guard.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +2 -1
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/turn-trigger-provenance.test.js +35 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -1
- package/dist/test/unconvertible-tool-result.test.d.ts +2 -0
- package/dist/test/unconvertible-tool-result.test.d.ts.map +1 -0
- package/dist/test/unconvertible-tool-result.test.js +358 -0
- package/dist/test/unconvertible-tool-result.test.js.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts +16 -0
- package/dist/test/workspace-materialize-freshness.test.d.ts.map +1 -0
- package/dist/test/workspace-materialize-freshness.test.js +252 -0
- package/dist/test/workspace-materialize-freshness.test.js.map +1 -0
- package/dist/test/workspace-three-way.test.d.ts +12 -0
- package/dist/test/workspace-three-way.test.d.ts.map +1 -0
- package/dist/test/workspace-three-way.test.js +3079 -0
- package/dist/test/workspace-three-way.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +120 -44
- package/src/code-execution/py-runner.ts +66 -9
- package/src/code-execution/runtime-py.ts +215 -38
- package/src/framework.ts +1661 -363
- package/src/gate/event-gate.ts +59 -7
- package/src/index.ts +11 -0
- package/src/mcpl/channel-registry.ts +24 -3
- package/src/mcpl/conversation-router.ts +34 -1
- package/src/mcpl/feature-set-manager.ts +36 -11
- package/src/mcpl/index.ts +2 -0
- package/src/mcpl/prose-stream-router.ts +79 -23
- package/src/mcpl/push-coalescer.ts +62 -13
- package/src/mcpl/push-handler.ts +41 -12
- package/src/mcpl/server-connection.ts +41 -10
- package/src/mcpl/server-registry.ts +24 -7
- package/src/mcpl/tool-lifecycle.ts +77 -2
- package/src/mcpl/tool-pattern-check.ts +106 -0
- package/src/mcpl/types.ts +15 -5
- package/src/mcpl/visible-content.ts +57 -0
- package/src/module-registry.ts +252 -16
- package/src/modules/workspace/branch-intent.ts +123 -0
- package/src/modules/workspace/disk-agreement.ts +325 -0
- package/src/modules/workspace/effects.ts +362 -0
- package/src/modules/workspace/index.ts +1058 -446
- package/src/modules/workspace/observe.ts +839 -0
- package/src/modules/workspace/reconcile.ts +1136 -0
- package/src/modules/workspace/sync.ts +6 -336
- package/src/modules/workspace/types.ts +75 -10
- package/src/record-journal.ts +246 -0
- package/src/shutdown.ts +49 -0
- package/src/silent-heartbeat.ts +141 -0
- package/src/tool-presentation.ts +188 -0
- package/src/tool-result-guard.ts +335 -33
- package/src/tool-result-history.ts +25 -1
- package/src/types/agent.ts +18 -2
- package/src/types/events.ts +4 -0
- package/src/types/framework.ts +12 -0
- package/src/types/module.ts +15 -0
- package/src/types/trace.ts +80 -0
|
@@ -1,350 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - materializeToFs: Chronicle → filesystem (agent changes)
|
|
2
|
+
* Shared helpers for moving content between the filesystem and the Chronicle
|
|
3
|
+
* tree. The rules for when content moves — the three-way rule over disk, the
|
|
4
|
+
* store and disk-agreement evidence — live in reconcile.ts, and how disk is
|
|
5
|
+
* observed (containment, symlinks, walks) in observe.ts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { createHash } from 'node:crypto';
|
|
10
|
-
import { readFile, writeFile, mkdir, readdir, stat, access } from 'node:fs/promises';
|
|
11
|
-
import { join, dirname, relative, resolve, sep } from 'node:path';
|
|
12
|
-
import type { JsStore, JsTreeEntry } from '@animalabs/chronicle';
|
|
13
|
-
import type { MountState } from './types.js';
|
|
14
9
|
|
|
15
10
|
export const DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
16
11
|
|
|
17
12
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
function safePath(mountPath: string, relativePath: string): string | null {
|
|
22
|
-
// Compare with the PLATFORM separator: resolve() emits backslashes on
|
|
23
|
-
// Windows, so a '/'-suffixed root never prefix-matches there and every
|
|
24
|
-
// in-mount path was reported as outside the mount.
|
|
25
|
-
const resolved = resolve(mountPath, relativePath);
|
|
26
|
-
const root = mountPath.endsWith(sep) ? mountPath : mountPath + sep;
|
|
27
|
-
if (resolved !== mountPath && !resolved.startsWith(root)) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
return resolved;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Hash file content to a full SHA-256 hex string.
|
|
35
|
-
* Must match Chronicle's storeBlob() hash format (64-char hex).
|
|
13
|
+
* Hash content to a full SHA-256 hex string.
|
|
14
|
+
* Matches Chronicle's storeBlob() hash format (64-char hex).
|
|
36
15
|
*/
|
|
37
16
|
export function hashContent(content: string | Buffer): string {
|
|
38
17
|
const hash = createHash('sha256');
|
|
39
18
|
hash.update(content);
|
|
40
19
|
return hash.digest('hex');
|
|
41
20
|
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Check if content appears to be binary.
|
|
45
|
-
*/
|
|
46
|
-
export interface SkippedFile {
|
|
47
|
-
/** Relative path of the file that was not synced */
|
|
48
|
-
path: string;
|
|
49
|
-
/** Why, in words the resident can act on */
|
|
50
|
-
reason: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function isBinary(buffer: Buffer): boolean {
|
|
54
|
-
// Check for null bytes in first 8KB
|
|
55
|
-
const check = buffer.subarray(0, 8192);
|
|
56
|
-
for (let i = 0; i < check.length; i++) {
|
|
57
|
-
if (check[i] === 0) return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface ConflictInfo {
|
|
63
|
-
/** Relative path of the conflicted file */
|
|
64
|
-
path: string;
|
|
65
|
-
/** Blob hash of the agent's version (retrievable via store.getBlob()) */
|
|
66
|
-
agentBlobHash: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type SyncOp = 'created' | 'modified' | 'deleted';
|
|
70
|
-
|
|
71
|
-
export interface SyncedPath {
|
|
72
|
-
path: string;
|
|
73
|
-
op: SyncOp;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface SyncResult {
|
|
77
|
-
/** Paths that were synced, tagged with the op detected against tree state */
|
|
78
|
-
synced: SyncedPath[];
|
|
79
|
-
/** Paths that conflicted (both agent and user modified) — filesystem wins */
|
|
80
|
-
conflicts: ConflictInfo[];
|
|
81
|
-
/** Paths that were skipped, each with the reason — a silently skipped file
|
|
82
|
-
* is indistinguishable from one that synced cleanly, which is how a
|
|
83
|
-
* resident ends up believing a file is in their workspace when it never
|
|
84
|
-
* arrived. */
|
|
85
|
-
skipped: SkippedFile[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Sync filesystem changes into Chronicle tree state.
|
|
90
|
-
*
|
|
91
|
-
* @param store Chronicle store
|
|
92
|
-
* @param mount Mount state
|
|
93
|
-
* @param paths Specific paths to sync (relative to mount). If empty, walks the directory.
|
|
94
|
-
* @returns Sync result with synced/conflicted/skipped paths
|
|
95
|
-
*/
|
|
96
|
-
export async function syncFromFs(
|
|
97
|
-
store: JsStore,
|
|
98
|
-
mount: MountState,
|
|
99
|
-
paths?: string[],
|
|
100
|
-
): Promise<SyncResult> {
|
|
101
|
-
const result: SyncResult = { synced: [], conflicts: [], skipped: [] };
|
|
102
|
-
const maxSize = mount.config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
103
|
-
|
|
104
|
-
const filesToSync = paths ?? await walkDirectory(mount.config.path, mount.config.ignore ?? []);
|
|
105
|
-
|
|
106
|
-
for (const relativePath of filesToSync) {
|
|
107
|
-
const absolutePath = safePath(mount.config.path, relativePath);
|
|
108
|
-
if (!absolutePath) {
|
|
109
|
-
result.skipped.push({ path: relativePath, reason: 'path resolves outside the mount' });
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await access(absolutePath);
|
|
115
|
-
} catch {
|
|
116
|
-
// File was deleted on disk — remove from tree
|
|
117
|
-
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
118
|
-
if (existing) {
|
|
119
|
-
store.treeRemove(mount.treeStateId, relativePath);
|
|
120
|
-
result.synced.push({ path: relativePath, op: 'deleted' });
|
|
121
|
-
}
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const fileStat = await stat(absolutePath);
|
|
127
|
-
if (!fileStat.isFile()) continue;
|
|
128
|
-
if (fileStat.size > maxSize) {
|
|
129
|
-
result.skipped.push({
|
|
130
|
-
path: relativePath,
|
|
131
|
-
reason: `too large: ${fileStat.size} bytes > maxFileSize ${maxSize}`,
|
|
132
|
-
});
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const buffer = await readFile(absolutePath);
|
|
137
|
-
if (isBinary(buffer)) {
|
|
138
|
-
// Disk -> store sync is text-only, at ANY size: the tree stores text
|
|
139
|
-
// blobs. Binary that must live in the store has to be written through
|
|
140
|
-
// writeBinary; binary that is only passing through should be read with
|
|
141
|
-
// readBinaryFromDisk instead of synced at all.
|
|
142
|
-
result.skipped.push({
|
|
143
|
-
path: relativePath,
|
|
144
|
-
reason: 'binary file — disk sync stores text only (read it from disk, or write it via a binary-aware tool)',
|
|
145
|
-
});
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const content = buffer.toString('utf-8');
|
|
150
|
-
const hash = hashContent(content);
|
|
151
|
-
|
|
152
|
-
// Check current tree state
|
|
153
|
-
const existing = store.treeGet(mount.treeStateId, relativePath);
|
|
154
|
-
|
|
155
|
-
if (existing && existing.blobHash === hash) {
|
|
156
|
-
// No change
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Store blob and update tree
|
|
161
|
-
const blobHash = store.storeBlob(Buffer.from(content, 'utf-8'), 'text/plain');
|
|
162
|
-
const entry: JsTreeEntry = {
|
|
163
|
-
blobHash,
|
|
164
|
-
size: buffer.length,
|
|
165
|
-
mode: 0o644,
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// Conflict detection: if the file existed in the tree AND the agent has
|
|
169
|
-
// modified it since last materialization, this is a genuine conflict
|
|
170
|
-
// (both agent and user changed the same file).
|
|
171
|
-
if (existing) {
|
|
172
|
-
const baselineHash = mount.materializedHashes.get(relativePath);
|
|
173
|
-
if (baselineHash && existing.blobHash !== baselineHash) {
|
|
174
|
-
// Agent changed the tree entry since we last materialized — conflict.
|
|
175
|
-
// Filesystem still wins, but agent's version is recoverable via agentBlobHash.
|
|
176
|
-
result.conflicts.push({
|
|
177
|
-
path: relativePath,
|
|
178
|
-
agentBlobHash: existing.blobHash,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
store.treeSet(mount.treeStateId, relativePath, entry);
|
|
184
|
-
result.synced.push({ path: relativePath, op: existing ? 'modified' : 'created' });
|
|
185
|
-
} catch (error) {
|
|
186
|
-
result.skipped.push({
|
|
187
|
-
path: relativePath,
|
|
188
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Materialize Chronicle tree state to filesystem.
|
|
198
|
-
*
|
|
199
|
-
* @param store Chronicle store
|
|
200
|
-
* @param mount Mount state
|
|
201
|
-
* @param paths Specific paths to materialize. If undefined, materializes all changed since last.
|
|
202
|
-
* @returns List of paths that were written
|
|
203
|
-
*/
|
|
204
|
-
export async function materializeToFs(
|
|
205
|
-
store: JsStore,
|
|
206
|
-
mount: MountState,
|
|
207
|
-
paths?: string[],
|
|
208
|
-
): Promise<string[]> {
|
|
209
|
-
if (mount.config.mode === 'read-only') {
|
|
210
|
-
return [];
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const written: string[] = [];
|
|
214
|
-
|
|
215
|
-
// Get changed files since last materialization
|
|
216
|
-
const currentSeq = store.currentSequence();
|
|
217
|
-
let filesToMaterialize: Array<{ path: string; blobHash: string }>;
|
|
218
|
-
|
|
219
|
-
if (paths) {
|
|
220
|
-
// Materialize specific paths
|
|
221
|
-
filesToMaterialize = [];
|
|
222
|
-
for (const p of paths) {
|
|
223
|
-
const entry = store.treeGet(mount.treeStateId, p);
|
|
224
|
-
if (entry) {
|
|
225
|
-
filesToMaterialize.push({ path: p, blobHash: entry.blobHash });
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} else if (mount.lastMaterializedSeq > 0) {
|
|
229
|
-
// Diff since last materialization
|
|
230
|
-
const changes = store.treeDiff(
|
|
231
|
-
mount.treeStateId,
|
|
232
|
-
mount.lastMaterializedSeq,
|
|
233
|
-
currentSeq,
|
|
234
|
-
);
|
|
235
|
-
filesToMaterialize = changes
|
|
236
|
-
.filter(c => c.changeType === 'added' || c.changeType === 'modified')
|
|
237
|
-
.map(c => ({
|
|
238
|
-
path: c.path,
|
|
239
|
-
blobHash: c.newEntry!.blobHash,
|
|
240
|
-
}));
|
|
241
|
-
|
|
242
|
-
// Handle removals
|
|
243
|
-
for (const change of changes) {
|
|
244
|
-
if (change.changeType === 'removed') {
|
|
245
|
-
// Don't delete from filesystem — just skip.
|
|
246
|
-
// Agent removing from tree doesn't mean delete user's file.
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
// First materialization — materialize everything
|
|
251
|
-
const entries = store.treeList(mount.treeStateId);
|
|
252
|
-
filesToMaterialize = entries.map(e => ({
|
|
253
|
-
path: e.path,
|
|
254
|
-
blobHash: e.blobHash,
|
|
255
|
-
}));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
for (const { path: relativePath, blobHash } of filesToMaterialize) {
|
|
259
|
-
const absolutePath = safePath(mount.config.path, relativePath);
|
|
260
|
-
if (!absolutePath) continue; // Path traversal — skip silently
|
|
261
|
-
|
|
262
|
-
const blob = store.getBlob(blobHash);
|
|
263
|
-
if (!blob) continue;
|
|
264
|
-
|
|
265
|
-
// Create parent directories
|
|
266
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
267
|
-
|
|
268
|
-
// Write file
|
|
269
|
-
await writeFile(absolutePath, blob);
|
|
270
|
-
written.push(relativePath);
|
|
271
|
-
|
|
272
|
-
// Record blob hash at materialization time for conflict detection
|
|
273
|
-
mount.materializedHashes.set(relativePath, blobHash);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
mount.lastMaterializedSeq = currentSeq;
|
|
277
|
-
|
|
278
|
-
return written;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Walk a directory recursively, respecting ignore patterns.
|
|
283
|
-
*/
|
|
284
|
-
async function walkDirectory(
|
|
285
|
-
basePath: string,
|
|
286
|
-
ignorePatterns: string[],
|
|
287
|
-
): Promise<string[]> {
|
|
288
|
-
const results: string[] = [];
|
|
289
|
-
const maxFiles = 5000; // Safety limit
|
|
290
|
-
|
|
291
|
-
async function walk(dir: string) {
|
|
292
|
-
if (results.length >= maxFiles) return;
|
|
293
|
-
|
|
294
|
-
let entries;
|
|
295
|
-
try {
|
|
296
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
297
|
-
} catch {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const entry of entries) {
|
|
302
|
-
if (results.length >= maxFiles) break;
|
|
303
|
-
|
|
304
|
-
const fullPath = join(dir, entry.name);
|
|
305
|
-
// Logical workspace paths are '/'-separated everywhere (mount-prefixed
|
|
306
|
-
// paths, chronicle tree paths); node's relative() emits backslashes on
|
|
307
|
-
// Windows, so normalize. No-op on POSIX (sep === '/').
|
|
308
|
-
const relativePath = relative(basePath, fullPath).split(sep).join('/');
|
|
309
|
-
|
|
310
|
-
// Check ignore patterns (simple glob matching)
|
|
311
|
-
if (shouldIgnore(relativePath, entry.name, ignorePatterns)) continue;
|
|
312
|
-
|
|
313
|
-
if (entry.isFile()) {
|
|
314
|
-
results.push(relativePath);
|
|
315
|
-
} else if (entry.isDirectory()) {
|
|
316
|
-
await walk(fullPath);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
await walk(basePath);
|
|
322
|
-
return results;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Simple ignore pattern matching.
|
|
327
|
-
*/
|
|
328
|
-
function shouldIgnore(
|
|
329
|
-
relativePath: string,
|
|
330
|
-
name: string,
|
|
331
|
-
patterns: string[],
|
|
332
|
-
): boolean {
|
|
333
|
-
for (const pattern of patterns) {
|
|
334
|
-
// Exact name match (e.g., ".git", "node_modules")
|
|
335
|
-
if (pattern === name) return true;
|
|
336
|
-
|
|
337
|
-
// Simple ** glob: "node_modules/**" matches anything under node_modules
|
|
338
|
-
if (pattern.endsWith('/**')) {
|
|
339
|
-
const prefix = pattern.slice(0, -3);
|
|
340
|
-
if (relativePath.startsWith(prefix + '/') || relativePath === prefix) return true;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Extension glob: "*.pyc" matches any .pyc file
|
|
344
|
-
if (pattern.startsWith('*.')) {
|
|
345
|
-
const ext = pattern.slice(1);
|
|
346
|
-
if (name.endsWith(ext)) return true;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
@@ -18,9 +18,13 @@ export interface MountConfig {
|
|
|
18
18
|
mode: 'read-write' | 'read-only';
|
|
19
19
|
/**
|
|
20
20
|
* Watch mode for filesystem changes:
|
|
21
|
-
* - 'always': chokidar watches continuously
|
|
22
|
-
* - 'on-agent-action':
|
|
23
|
-
*
|
|
21
|
+
* - 'always': chokidar watches continuously; each event re-observes its paths
|
|
22
|
+
* - 'on-agent-action': after each completed agent tool batch, the mount is
|
|
23
|
+
* scanned, and the agent's next inference waits for that scan up to
|
|
24
|
+
* `agentActionScanDeadlineMs`; a scan that takes longer finishes while
|
|
25
|
+
* the round goes on
|
|
26
|
+
* - 'never': no background reads; listings and lazy reads still observe
|
|
27
|
+
* the paths they show
|
|
24
28
|
*/
|
|
25
29
|
watch?: 'always' | 'on-agent-action' | 'never';
|
|
26
30
|
/** Debounce window in ms for watch: 'always' mode (default: 300) */
|
|
@@ -72,6 +76,19 @@ export interface WorkspaceConfig {
|
|
|
72
76
|
deltaSnapshotEvery?: number;
|
|
73
77
|
/** Full snapshot frequency for tree states (default: 10) */
|
|
74
78
|
fullSnapshotEvery?: number;
|
|
79
|
+
/**
|
|
80
|
+
* How long the scan of `watch: 'on-agent-action'` mounts after a tool batch
|
|
81
|
+
* may hold the agent's next inference, in ms (default: 20000). A scan that
|
|
82
|
+
* takes longer finishes in the background; the miss is recorded on the
|
|
83
|
+
* mount's status and pushed as a `workspace:agent-action-scan-incomplete`
|
|
84
|
+
* event. So is a scan that finished without observing everything, with the
|
|
85
|
+
* regions it couldn't (`incomplete`). While a scan that outlasted its
|
|
86
|
+
* deadline still runs, later batches aren't held: their scan is queued
|
|
87
|
+
* after it, and the miss is recorded at once. The framework bounds every
|
|
88
|
+
* module's batch hook at 30 s, so a deadline above 29 s is lowered to 29 s,
|
|
89
|
+
* with a warning, for the workspace's own miss to come first.
|
|
90
|
+
*/
|
|
91
|
+
agentActionScanDeadlineMs?: number;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
// ============================================================================
|
|
@@ -79,23 +96,52 @@ export interface WorkspaceConfig {
|
|
|
79
96
|
// ============================================================================
|
|
80
97
|
|
|
81
98
|
/**
|
|
82
|
-
* Per-mount runtime state (not persisted — rebuilt on start).
|
|
99
|
+
* Per-mount runtime state (not persisted — rebuilt on start). What disk last
|
|
100
|
+
* agreed with lives in the module's disk-agreement journal (global chronicle
|
|
101
|
+
* records), and branch-local intent in `intentTreeStateId`.
|
|
83
102
|
*/
|
|
84
103
|
export interface MountState {
|
|
85
104
|
/** The mount config */
|
|
86
105
|
config: MountConfig;
|
|
87
106
|
/** Tree state ID in Chronicle */
|
|
88
107
|
treeStateId: string;
|
|
108
|
+
/** Branch-local intent (tombstones, store origin, conflicts) for this mount's paths */
|
|
109
|
+
intentTreeStateId: string;
|
|
89
110
|
/** Sequence number of last materialization */
|
|
90
111
|
lastMaterializedSeq: number;
|
|
91
112
|
/** Paths currently suppressed from watcher (recently materialized) */
|
|
92
113
|
suppressedPaths: Set<string>;
|
|
93
|
-
/** Whether
|
|
114
|
+
/** Whether a full scan of the mount has completed this session */
|
|
94
115
|
initialSyncDone: boolean;
|
|
95
116
|
/** Branch ID that was active when this mount last materialized */
|
|
96
117
|
lastMaterializedBranchId: string | null;
|
|
97
|
-
/**
|
|
98
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The last on-agent-action scan, as of `at`: `complete` when it has
|
|
120
|
+
* finished at all, `withinDeadline` when it finished before the deadline
|
|
121
|
+
* released the round, `reason` for a miss or a failure, and `incomplete`
|
|
122
|
+
* for the regions a finished scan couldn't observe (each also pushed as a
|
|
123
|
+
* `workspace:agent-action-scan-incomplete` event).
|
|
124
|
+
*/
|
|
125
|
+
lastAgentActionScan?: {
|
|
126
|
+
at: number;
|
|
127
|
+
complete: boolean;
|
|
128
|
+
withinDeadline: boolean;
|
|
129
|
+
reason?: string;
|
|
130
|
+
incomplete?: Array<{ path: string; reason: string }>;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The after-batch scan queued behind the mount's running work, not yet
|
|
134
|
+
* started: later batches share it, since it starts after their tools ran.
|
|
135
|
+
*/
|
|
136
|
+
agentActionScanWaiting?: AgentActionScan;
|
|
137
|
+
/**
|
|
138
|
+
* The mount's newest after-batch scan. Only its outcome is put on
|
|
139
|
+
* `lastAgentActionScan`: one queued behind an older scan describes a later
|
|
140
|
+
* batch, which the older scan's outcome mustn't speak for.
|
|
141
|
+
*/
|
|
142
|
+
agentActionScanLatest?: AgentActionScan;
|
|
143
|
+
/** A scan that outlasted a batch's deadline, while it still runs. */
|
|
144
|
+
agentActionScanLate?: Promise<unknown>;
|
|
99
145
|
/**
|
|
100
146
|
* Wall-clock time chokidar emitted `ready` for this mount, or null if the
|
|
101
147
|
* watcher hasn't finished its initial scan. null after session start =
|
|
@@ -195,6 +241,14 @@ export interface GrepInput {
|
|
|
195
241
|
contextAfter?: number;
|
|
196
242
|
}
|
|
197
243
|
|
|
244
|
+
/** One after-batch scan of a mount, shared by the batches that wait for it. */
|
|
245
|
+
export interface AgentActionScan {
|
|
246
|
+
/** Whether some batch stopped waiting for it (its deadline passed, or an earlier scan still ran late). */
|
|
247
|
+
late: boolean;
|
|
248
|
+
/** Settles, never rejects, once its outcome is recorded: on status if it is the newest, as events either way. */
|
|
249
|
+
settled: Promise<void>;
|
|
250
|
+
}
|
|
251
|
+
|
|
198
252
|
export interface StatusInput {
|
|
199
253
|
/** Specific mount to check (optional — defaults to all) */
|
|
200
254
|
mount?: string;
|
|
@@ -207,11 +261,18 @@ export interface MaterializeInput {
|
|
|
207
261
|
mount?: string;
|
|
208
262
|
/**
|
|
209
263
|
* Materialize even when the current branch has genuinely diverged from the
|
|
210
|
-
* branch last materialized to disk
|
|
211
|
-
*
|
|
212
|
-
* point) — those pass the guard
|
|
264
|
+
* branch last materialized to disk, and overwrite a disk copy in conflict
|
|
265
|
+
* (default: false). Not needed for linear continuations (child branches
|
|
266
|
+
* forked at or after the last materialized point) — those pass the guard
|
|
267
|
+
* automatically.
|
|
213
268
|
*/
|
|
214
269
|
force?: boolean;
|
|
270
|
+
/**
|
|
271
|
+
* Also delete from disk the files deleted in the workspace whose disk copy
|
|
272
|
+
* is still one the workspace holds (default: false). With `force`, also
|
|
273
|
+
* those whose disk copy changed since.
|
|
274
|
+
*/
|
|
275
|
+
applyDeletions?: boolean;
|
|
215
276
|
}
|
|
216
277
|
|
|
217
278
|
export interface SyncInput {
|
|
@@ -219,6 +280,8 @@ export interface SyncInput {
|
|
|
219
280
|
path?: string;
|
|
220
281
|
/** Specific mount (optional — defaults to all) */
|
|
221
282
|
mount?: string;
|
|
283
|
+
/** Take each mount's root as it is now, when it isn't the directory disk last agreed with. */
|
|
284
|
+
acceptRoot?: boolean;
|
|
222
285
|
}
|
|
223
286
|
|
|
224
287
|
// ============================================================================
|
|
@@ -252,6 +315,8 @@ export interface WorkspaceDeletedEvent {
|
|
|
252
315
|
type: 'workspace:deleted';
|
|
253
316
|
paths: string[];
|
|
254
317
|
mount: string;
|
|
318
|
+
/** Paths deleted on disk whose newer workspace version was kept as a conflict. */
|
|
319
|
+
conflicts?: string[];
|
|
255
320
|
[key: string]: unknown;
|
|
256
321
|
}
|
|
257
322
|
|