@beeos-ai/device-agent 0.4.3 → 0.5.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.
Files changed (180) hide show
  1. package/.pi/extensions/android-tools.ts +146 -15
  2. package/dist/adapters/agent-gateway-client.d.ts +37 -0
  3. package/dist/adapters/agent-gateway-client.js +45 -0
  4. package/dist/adapters/agent-gateway-client.js.map +1 -1
  5. package/dist/adapters/agentcore-pi.d.ts +22 -0
  6. package/dist/adapters/agentcore-pi.js +48 -0
  7. package/dist/adapters/agentcore-pi.js.map +1 -1
  8. package/dist/adapters/device-android-adb.d.ts +47 -0
  9. package/dist/adapters/device-android-adb.js +50 -0
  10. package/dist/adapters/device-android-adb.js.map +1 -1
  11. package/dist/adapters/device-desktop.d.ts +12 -0
  12. package/dist/adapters/device-desktop.js +31 -0
  13. package/dist/adapters/device-desktop.js.map +1 -1
  14. package/dist/agent-runtime/adapters/base.d.ts +20 -0
  15. package/dist/agent-runtime/adapters/base.js.map +1 -1
  16. package/dist/agent-runtime/agent-runner.d.ts +4 -2
  17. package/dist/agent-runtime/agent-runner.js.map +1 -1
  18. package/dist/agent-runtime/core.d.ts +60 -0
  19. package/dist/agent-runtime/core.js +190 -9
  20. package/dist/agent-runtime/core.js.map +1 -1
  21. package/dist/agent-runtime/feedback.d.ts +56 -0
  22. package/dist/agent-runtime/feedback.js +54 -0
  23. package/dist/agent-runtime/feedback.js.map +1 -0
  24. package/dist/agent-runtime/index.d.ts +1 -0
  25. package/dist/agent-runtime/index.js +1 -0
  26. package/dist/agent-runtime/index.js.map +1 -1
  27. package/dist/agent-runtime/plan-executor.js +24 -0
  28. package/dist/agent-runtime/plan-executor.js.map +1 -1
  29. package/dist/agent-runtime/planner/base.d.ts +10 -0
  30. package/dist/agent-runtime/planner/base.js.map +1 -1
  31. package/dist/agent-runtime/planner/llm-planner.js +1 -0
  32. package/dist/agent-runtime/planner/llm-planner.js.map +1 -1
  33. package/dist/agent-runtime/planner/prompts.d.ts +3 -0
  34. package/dist/agent-runtime/planner/prompts.js +12 -0
  35. package/dist/agent-runtime/planner/prompts.js.map +1 -1
  36. package/dist/agent-runtime/reflection.d.ts +25 -6
  37. package/dist/agent-runtime/reflection.js +64 -22
  38. package/dist/agent-runtime/reflection.js.map +1 -1
  39. package/dist/agent-runtime/reflectors/policy.d.ts +10 -4
  40. package/dist/agent-runtime/reflectors/policy.js +19 -6
  41. package/dist/agent-runtime/reflectors/policy.js.map +1 -1
  42. package/dist/agent-runtime/shortcuts/fs-store.d.ts +41 -0
  43. package/dist/agent-runtime/shortcuts/fs-store.js +122 -0
  44. package/dist/agent-runtime/shortcuts/fs-store.js.map +1 -0
  45. package/dist/agent-runtime/shortcuts/in-memory-store.d.ts +20 -0
  46. package/dist/agent-runtime/shortcuts/in-memory-store.js +64 -0
  47. package/dist/agent-runtime/shortcuts/in-memory-store.js.map +1 -0
  48. package/dist/agent-runtime/shortcuts/index.d.ts +10 -0
  49. package/dist/agent-runtime/shortcuts/index.js +10 -0
  50. package/dist/agent-runtime/shortcuts/index.js.map +1 -0
  51. package/dist/agent-runtime/shortcuts/types.d.ts +69 -0
  52. package/dist/agent-runtime/shortcuts/types.js +17 -0
  53. package/dist/agent-runtime/shortcuts/types.js.map +1 -0
  54. package/dist/app/agent-host.d.ts +83 -0
  55. package/dist/app/agent-host.js +345 -32
  56. package/dist/app/agent-host.js.map +1 -1
  57. package/dist/app/session-manager.d.ts +33 -4
  58. package/dist/app/session-manager.js +27 -5
  59. package/dist/app/session-manager.js.map +1 -1
  60. package/dist/bootstrap.js +187 -3
  61. package/dist/bootstrap.js.map +1 -1
  62. package/dist/config/loader.js +22 -0
  63. package/dist/config/loader.js.map +1 -1
  64. package/dist/config/schema.d.ts +18 -0
  65. package/dist/config/schema.js +118 -0
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/inbound/acp/handlers/session.js.map +1 -1
  68. package/dist/mcp-client/client.d.ts +6 -0
  69. package/dist/mcp-client/client.js +11 -0
  70. package/dist/mcp-client/client.js.map +1 -1
  71. package/dist/mcp-client/runtime.d.ts +13 -0
  72. package/dist/orchestrator-system-prompt.d.ts +26 -0
  73. package/dist/orchestrator-system-prompt.js +118 -2
  74. package/dist/orchestrator-system-prompt.js.map +1 -1
  75. package/dist/pi-context.d.ts +50 -0
  76. package/dist/pi-context.js.map +1 -1
  77. package/dist/ports/device.d.ts +18 -0
  78. package/dist/resources/augmenter.d.ts +48 -0
  79. package/dist/resources/augmenter.js +126 -0
  80. package/dist/resources/augmenter.js.map +1 -0
  81. package/dist/resources/cache.d.ts +57 -0
  82. package/dist/resources/cache.js +107 -0
  83. package/dist/resources/cache.js.map +1 -0
  84. package/dist/resources/index.d.ts +17 -0
  85. package/dist/resources/index.js +15 -0
  86. package/dist/resources/index.js.map +1 -0
  87. package/dist/resources/lifecycle/disk-pressure.d.ts +75 -0
  88. package/dist/resources/lifecycle/disk-pressure.js +148 -0
  89. package/dist/resources/lifecycle/disk-pressure.js.map +1 -0
  90. package/dist/resources/lifecycle/index.d.ts +7 -0
  91. package/dist/resources/lifecycle/index.js +7 -0
  92. package/dist/resources/lifecycle/index.js.map +1 -0
  93. package/dist/resources/lifecycle/manager.d.ts +38 -0
  94. package/dist/resources/lifecycle/manager.js +205 -0
  95. package/dist/resources/lifecycle/manager.js.map +1 -0
  96. package/dist/resources/lifecycle/sweeper.d.ts +36 -0
  97. package/dist/resources/lifecycle/sweeper.js +179 -0
  98. package/dist/resources/lifecycle/sweeper.js.map +1 -0
  99. package/dist/resources/lifecycle/types.d.ts +180 -0
  100. package/dist/resources/lifecycle/types.js +46 -0
  101. package/dist/resources/lifecycle/types.js.map +1 -0
  102. package/dist/resources/parsers.d.ts +28 -0
  103. package/dist/resources/parsers.js +88 -0
  104. package/dist/resources/parsers.js.map +1 -0
  105. package/dist/resources/registry.d.ts +47 -0
  106. package/dist/resources/registry.js +107 -0
  107. package/dist/resources/registry.js.map +1 -0
  108. package/dist/resources/resolvers/beeos-file-resolver.d.ts +62 -0
  109. package/dist/resources/resolvers/beeos-file-resolver.js +109 -0
  110. package/dist/resources/resolvers/beeos-file-resolver.js.map +1 -0
  111. package/dist/resources/resolvers/errors.d.ts +11 -0
  112. package/dist/resources/resolvers/errors.js +23 -0
  113. package/dist/resources/resolvers/errors.js.map +1 -0
  114. package/dist/resources/resolvers/mime-table.d.ts +41 -0
  115. package/dist/resources/resolvers/mime-table.js +92 -0
  116. package/dist/resources/resolvers/mime-table.js.map +1 -0
  117. package/dist/resources/resolvers/url-resolver.d.ts +85 -0
  118. package/dist/resources/resolvers/url-resolver.js +495 -0
  119. package/dist/resources/resolvers/url-resolver.js.map +1 -0
  120. package/dist/resources/shortcut-facade.d.ts +51 -0
  121. package/dist/resources/shortcut-facade.js +72 -0
  122. package/dist/resources/shortcut-facade.js.map +1 -0
  123. package/dist/resources/telemetry.d.ts +52 -0
  124. package/dist/resources/telemetry.js +59 -0
  125. package/dist/resources/telemetry.js.map +1 -0
  126. package/dist/resources/types.d.ts +249 -0
  127. package/dist/resources/types.js +29 -0
  128. package/dist/resources/types.js.map +1 -0
  129. package/dist/shortcuts/builtins/download-to-device.d.ts +83 -0
  130. package/dist/shortcuts/builtins/download-to-device.js +125 -0
  131. package/dist/shortcuts/builtins/download-to-device.js.map +1 -0
  132. package/dist/shortcuts/builtins/find-app-package.d.ts +79 -0
  133. package/dist/shortcuts/builtins/find-app-package.js +168 -0
  134. package/dist/shortcuts/builtins/find-app-package.js.map +1 -0
  135. package/dist/shortcuts/builtins/index.d.ts +32 -0
  136. package/dist/shortcuts/builtins/index.js +37 -0
  137. package/dist/shortcuts/builtins/index.js.map +1 -0
  138. package/dist/shortcuts/builtins/open-deeplink.d.ts +68 -0
  139. package/dist/shortcuts/builtins/open-deeplink.js +142 -0
  140. package/dist/shortcuts/builtins/open-deeplink.js.map +1 -0
  141. package/dist/shortcuts/builtins/set-clipboard.d.ts +65 -0
  142. package/dist/shortcuts/builtins/set-clipboard.js +112 -0
  143. package/dist/shortcuts/builtins/set-clipboard.js.map +1 -0
  144. package/dist/shortcuts/index.d.ts +11 -0
  145. package/dist/shortcuts/index.js +11 -0
  146. package/dist/shortcuts/index.js.map +1 -0
  147. package/dist/shortcuts/registry.d.ts +56 -0
  148. package/dist/shortcuts/registry.js +70 -0
  149. package/dist/shortcuts/registry.js.map +1 -0
  150. package/dist/shortcuts/router.d.ts +91 -0
  151. package/dist/shortcuts/router.js +158 -0
  152. package/dist/shortcuts/router.js.map +1 -0
  153. package/dist/shortcuts/types.d.ts +219 -0
  154. package/dist/shortcuts/types.js +46 -0
  155. package/dist/shortcuts/types.js.map +1 -0
  156. package/package.json +3 -3
  157. package/dist/agent-runtime/skills/sqlite-store.d.ts +0 -60
  158. package/dist/agent-runtime/skills/sqlite-store.js +0 -234
  159. package/dist/agent-runtime/skills/sqlite-store.js.map +0 -1
  160. package/dist/test-lan-autoglm.d.ts +0 -7
  161. package/dist/test-lan-autoglm.js +0 -46
  162. package/dist/test-lan-autoglm.js.map +0 -1
  163. package/dist/test-mem-breakdown.d.ts +0 -1
  164. package/dist/test-mem-breakdown.js +0 -103
  165. package/dist/test-mem-breakdown.js.map +0 -1
  166. package/dist/test-mem.d.ts +0 -1
  167. package/dist/test-mem.js +0 -60
  168. package/dist/test-mem.js.map +0 -1
  169. package/dist/test-mobile-use.d.ts +0 -1
  170. package/dist/test-mobile-use.js +0 -18
  171. package/dist/test-mobile-use.js.map +0 -1
  172. package/dist/test-screenshot-loop.d.ts +0 -1
  173. package/dist/test-screenshot-loop.js +0 -43
  174. package/dist/test-screenshot-loop.js.map +0 -1
  175. package/dist/test-screenshot.d.ts +0 -1
  176. package/dist/test-screenshot.js +0 -10
  177. package/dist/test-screenshot.js.map +0 -1
  178. package/dist/test-ui-xml-dump.d.ts +0 -15
  179. package/dist/test-ui-xml-dump.js +0 -49
  180. package/dist/test-ui-xml-dump.js.map +0 -1
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Resource Resolution Cache — keyed by SHA-256(url), value is a
3
+ * `ResolvedAsset`. Cache hits MUST validate the device-side file is
4
+ * still there before reuse.
5
+ *
6
+ * Why an in-memory cache and not skill memory?
7
+ * - Skill memory is per-app GUI sequences (see ADR 001). Resolutions
8
+ * are app-agnostic and have a much shorter natural lifetime
9
+ * (TTL=7d) — different concern, different data shape.
10
+ * - The cache lives for the AgentHost lifetime; we deliberately do
11
+ * NOT persist it to disk for Phase 1. A future plan may add an FS
12
+ * mirror under `~/.beeos/device-agent/resources-cache/` once the
13
+ * access pattern stabilises.
14
+ *
15
+ * Cache validation:
16
+ * When a hit is returned, the consumer is REQUIRED to call
17
+ * `validateAlive(asset, mcp)` to confirm the device-side file still
18
+ * exists (operators occasionally clear `/sdcard/Pictures/` between
19
+ * tasks). On a missing file, the cache entry is invalidated and the
20
+ * resolution is re-run.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
24
+ const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
25
+ export class ResourceResolutionCache {
26
+ ttlMs;
27
+ maxBytes;
28
+ entries = new Map();
29
+ constructor(opts = {}) {
30
+ this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
31
+ this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
32
+ }
33
+ /** Stable cache key — SHA-256 of the URL. */
34
+ static keyForUrl(url) {
35
+ return createHash("sha256").update(url).digest("hex");
36
+ }
37
+ get(key, now = Date.now()) {
38
+ const e = this.entries.get(key);
39
+ if (!e)
40
+ return undefined;
41
+ if (now - e.storedAt > this.ttlMs) {
42
+ this.entries.delete(key);
43
+ return undefined;
44
+ }
45
+ e.lastUsed = now;
46
+ return e.asset;
47
+ }
48
+ set(key, asset, now = Date.now()) {
49
+ this.entries.set(key, { asset, storedAt: now, lastUsed: now });
50
+ this.evictIfNeeded();
51
+ }
52
+ invalidate(key) {
53
+ this.entries.delete(key);
54
+ }
55
+ size() {
56
+ return this.entries.size;
57
+ }
58
+ totalBytes() {
59
+ let sum = 0;
60
+ for (const e of this.entries.values())
61
+ sum += e.asset.sizeBytes;
62
+ return sum;
63
+ }
64
+ /** Test helper — visible-for-spec, not for production callers. */
65
+ __debug() {
66
+ return { size: this.size(), totalBytes: this.totalBytes() };
67
+ }
68
+ /**
69
+ * Verify the device-side file referenced by a cached entry is still
70
+ * present. Returns `true` iff `ls <remotePath>` succeeds with non-
71
+ * empty output (or an explicit success exit code).
72
+ *
73
+ * MUST be called by every consumer that hits the cache before
74
+ * returning the asset to upstream code.
75
+ */
76
+ static async validateAlive(asset, mcp) {
77
+ if (typeof mcp.executeCommand !== "function") {
78
+ // No `executeCommand` ⇒ we cannot verify ⇒ pessimistic: treat
79
+ // as missing so we re-resolve. This keeps stale cache entries
80
+ // from sneaking into the in-proc legacy bridge.
81
+ return false;
82
+ }
83
+ try {
84
+ const r = await mcp.executeCommand(`ls -1 ${shellQuote(asset.remotePath)} 2>/dev/null`);
85
+ const exit = r.exitCode ?? 0;
86
+ const stdout = (r.stdout ?? "").trim();
87
+ return exit === 0 && stdout.length > 0;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ evictIfNeeded() {
94
+ if (this.totalBytes() <= this.maxBytes)
95
+ return;
96
+ // LRU by lastUsed.
97
+ const sorted = [...this.entries.entries()].sort((a, b) => a[1].lastUsed - b[1].lastUsed);
98
+ while (this.totalBytes() > this.maxBytes && sorted.length > 0) {
99
+ const [key] = sorted.shift();
100
+ this.entries.delete(key);
101
+ }
102
+ }
103
+ }
104
+ function shellQuote(s) {
105
+ return `'${s.replace(/'/g, "'\\''")}'`;
106
+ }
107
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/resources/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * resources/ — Resource Resolution Layer barrel.
3
+ *
4
+ * See `types.ts` for the protocol overview.
5
+ */
6
+ export type { ResourceRef, ResolvedAsset, ResolverContext, ResolveProgressEvent, ResolverLogger, ResourceResolver, ResourceParser, TaskContextAugmenter, ResolveOutcomeLabel, } from "./types.js";
7
+ export { UrlParser, BeeosFileParser, defaultParsers } from "./parsers.js";
8
+ export { ResourceContextAugmenter } from "./augmenter.js";
9
+ export { ResourceResolverRegistry, type ResolveAllResult, type ResolverRegistryOptions, } from "./registry.js";
10
+ export { UrlResolver, ResolverError, isPrivateOrLoopback, type UrlResolverOptions, } from "./resolvers/url-resolver.js";
11
+ export { BeeosFileResolver, type BeeosFileResolverOptions, } from "./resolvers/beeos-file-resolver.js";
12
+ export { ResourceResolutionCache, type ResourceCacheOptions, } from "./cache.js";
13
+ export { ALLOWED_MIMES, ALLOWED_MIME_PREFIXES, MIME_TO_EXT, mapMimeToExt, } from "./resolvers/mime-table.js";
14
+ export { AGENT_HOST_ERROR_LABELS, type ResolverErrorCode, } from "./resolvers/errors.js";
15
+ export type { DiskPressureGuard, DiskPressureCheckArgs, } from "./types.js";
16
+ export type { LifecycleMode, ResourceLifecycleConfig, ResourceLifecycleManager, ResourceLifecycleSweeper, LifecycleWiring, } from "./lifecycle/index.js";
17
+ export { FsResourceLifecycleManager, FsResourceLifecycleSweeper, FsDiskPressureGuard, parseDfFreeMb, type DiskPressureGuardOptions, } from "./lifecycle/index.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * resources/ — Resource Resolution Layer barrel.
3
+ *
4
+ * See `types.ts` for the protocol overview.
5
+ */
6
+ export { UrlParser, BeeosFileParser, defaultParsers } from "./parsers.js";
7
+ export { ResourceContextAugmenter } from "./augmenter.js";
8
+ export { ResourceResolverRegistry, } from "./registry.js";
9
+ export { UrlResolver, ResolverError, isPrivateOrLoopback, } from "./resolvers/url-resolver.js";
10
+ export { BeeosFileResolver, } from "./resolvers/beeos-file-resolver.js";
11
+ export { ResourceResolutionCache, } from "./cache.js";
12
+ export { ALLOWED_MIMES, ALLOWED_MIME_PREFIXES, MIME_TO_EXT, mapMimeToExt, } from "./resolvers/mime-table.js";
13
+ export { AGENT_HOST_ERROR_LABELS, } from "./resolvers/errors.js";
14
+ export { FsResourceLifecycleManager, FsResourceLifecycleSweeper, FsDiskPressureGuard, parseDfFreeMb, } from "./lifecycle/index.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,wBAAwB,GAGzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,WAAW,EACX,aAAa,EACb,mBAAmB,GAEpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,iBAAiB,GAElB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,uBAAuB,GAExB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,WAAW,EACX,YAAY,GACb,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,uBAAuB,GAExB,MAAM,uBAAuB,CAAC;AAY/B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,mBAAmB,EACnB,aAAa,GAEd,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Resource Lifecycle — L3 (disk-pressure adaptive sweep).
3
+ *
4
+ * Runs immediately before each `mcp.fileWrite` in the resolver. We
5
+ * read free space on the sandbox volume via `df`; when free MB drops
6
+ * below `diskMinFreeMb` we trigger a force-sweep (the same path the
7
+ * L2 timer uses) so the daemon doesn't fill the device's external
8
+ * storage on a long burst of downloads. If after sweeping free space
9
+ * is *still* below `diskCriticalFreeMb`, we abort the resolve with
10
+ * a structured `disk_pressure` error — that's the resolver's signal
11
+ * to fail fast rather than write a partial file the picker will
12
+ * never see.
13
+ *
14
+ * The two thresholds form a hysteresis band:
15
+ * - `diskMinFreeMb` — soft, triggers eviction.
16
+ * - `diskCriticalFreeMb` — hard, triggers rejection.
17
+ * Operators are responsible for keeping `critical < min`; if the two
18
+ * cross over, L3 still fail-softs (an over-strict critical just means
19
+ * a few extra rejections under pressure, never silent corruption).
20
+ *
21
+ * Why we parse `df` output ourselves:
22
+ * Android's BusyBox `df` does not have a `--output=avail` switch
23
+ * the way GNU coreutils does. The `-k` flag gives a stable
24
+ * `Filesystem 1K-blocks Used Available …` header on every BusyBox
25
+ * build I've seen since Android 7. We strip the header and read
26
+ * the column at index 3 (zero-indexed) of the **last** non-empty
27
+ * line. Unknown shapes return `null` and `assertCapacity` fails
28
+ * soft (warn + allow write) rather than rejecting downloads on a
29
+ * parse hiccup.
30
+ */
31
+ import type { IMcpRuntime } from "../../mcp-client/runtime.js";
32
+ import type { DiskPressureGuard, DiskPressureCheckArgs, ResolverLogger } from "../types.js";
33
+ import type { ResourceLifecycleSweeper } from "./types.js";
34
+ export interface DiskPressureGuardOptions {
35
+ /** Soft threshold — below this we force a sweep. */
36
+ readonly minFreeMb: number;
37
+ /** Hard threshold — below this AFTER a sweep we reject. */
38
+ readonly criticalFreeMb: number;
39
+ /** Sweeper used for the force-sweep step. */
40
+ readonly sweeper: ResourceLifecycleSweeper;
41
+ /**
42
+ * Optional: skip the live `df` shell call when known to be safe.
43
+ * Useful for fleet operators that have provisioned a dedicated
44
+ * /sdcard volume and want to disable the L3 layer.
45
+ */
46
+ readonly enabled?: boolean;
47
+ }
48
+ export declare class FsDiskPressureGuard implements DiskPressureGuard {
49
+ private readonly opts;
50
+ constructor(opts: DiskPressureGuardOptions);
51
+ assertCapacity(args: DiskPressureCheckArgs): Promise<void>;
52
+ /**
53
+ * Returns free MB on the volume containing `sandboxDir`, or `null`
54
+ * if `df` output couldn't be parsed. Visible-for-test.
55
+ */
56
+ dfFreeMb(mcp: IMcpRuntime, sandboxDir: string, log: ResolverLogger): Promise<number | null>;
57
+ }
58
+ /**
59
+ * Parse a `df -k` output and return free MB on the relevant volume,
60
+ * or `null` if the output doesn't match any known shape.
61
+ *
62
+ * Android BusyBox `df -k /sdcard` emits something like:
63
+ *
64
+ * Filesystem 1K-blocks Used Available Use% Mounted on
65
+ * /dev/block/dm-25 116228032 74283456 41789120 64% /storage/emulated
66
+ *
67
+ * The "Available" column is the 4th whitespace-separated field. We
68
+ * walk lines from the bottom up and return the first row with four
69
+ * numeric-looking fields after the device name. Anything else
70
+ * returns `null` and the L3 guard fails soft.
71
+ *
72
+ * Visible-for-test (exported as a pure function so unit tests don't
73
+ * need an MCP stub).
74
+ */
75
+ export declare function parseDfFreeMb(out: string): number | null;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Resource Lifecycle — L3 (disk-pressure adaptive sweep).
3
+ *
4
+ * Runs immediately before each `mcp.fileWrite` in the resolver. We
5
+ * read free space on the sandbox volume via `df`; when free MB drops
6
+ * below `diskMinFreeMb` we trigger a force-sweep (the same path the
7
+ * L2 timer uses) so the daemon doesn't fill the device's external
8
+ * storage on a long burst of downloads. If after sweeping free space
9
+ * is *still* below `diskCriticalFreeMb`, we abort the resolve with
10
+ * a structured `disk_pressure` error — that's the resolver's signal
11
+ * to fail fast rather than write a partial file the picker will
12
+ * never see.
13
+ *
14
+ * The two thresholds form a hysteresis band:
15
+ * - `diskMinFreeMb` — soft, triggers eviction.
16
+ * - `diskCriticalFreeMb` — hard, triggers rejection.
17
+ * Operators are responsible for keeping `critical < min`; if the two
18
+ * cross over, L3 still fail-softs (an over-strict critical just means
19
+ * a few extra rejections under pressure, never silent corruption).
20
+ *
21
+ * Why we parse `df` output ourselves:
22
+ * Android's BusyBox `df` does not have a `--output=avail` switch
23
+ * the way GNU coreutils does. The `-k` flag gives a stable
24
+ * `Filesystem 1K-blocks Used Available …` header on every BusyBox
25
+ * build I've seen since Android 7. We strip the header and read
26
+ * the column at index 3 (zero-indexed) of the **last** non-empty
27
+ * line. Unknown shapes return `null` and `assertCapacity` fails
28
+ * soft (warn + allow write) rather than rejecting downloads on a
29
+ * parse hiccup.
30
+ */
31
+ import { ResolverError } from "../resolvers/url-resolver.js";
32
+ export class FsDiskPressureGuard {
33
+ opts;
34
+ constructor(opts) {
35
+ this.opts = opts;
36
+ }
37
+ async assertCapacity(args) {
38
+ if (this.opts.enabled === false)
39
+ return;
40
+ if (typeof args.mcp.executeCommand !== "function")
41
+ return;
42
+ const initialFreeMb = await this.dfFreeMb(args.mcp, args.sandboxDir, args.log);
43
+ if (initialFreeMb === null) {
44
+ // df failed → log + skip (we'd rather over-write than reject
45
+ // every download because of a parse hiccup).
46
+ args.log.warn({ sandboxDir: args.sandboxDir }, "disk-pressure: df parse failed; allowing write");
47
+ return;
48
+ }
49
+ const incomingMb = Math.ceil(args.incomingBytes / (1024 * 1024));
50
+ const projectedFreeMb = initialFreeMb - incomingMb;
51
+ if (projectedFreeMb >= this.opts.minFreeMb) {
52
+ // Plenty of headroom.
53
+ return;
54
+ }
55
+ args.log.info({
56
+ initialFreeMb,
57
+ incomingMb,
58
+ projectedFreeMb,
59
+ minFreeMb: this.opts.minFreeMb,
60
+ }, "disk-pressure: low headroom, triggering force-sweep");
61
+ let deleted = 0;
62
+ try {
63
+ deleted = await this.opts.sweeper.sweepNow();
64
+ }
65
+ catch (e) {
66
+ args.log.warn({ err: errString(e) }, "disk-pressure: force-sweep threw (continuing)");
67
+ }
68
+ const postFreeMb = await this.dfFreeMb(args.mcp, args.sandboxDir, args.log);
69
+ if (postFreeMb === null) {
70
+ args.log.warn({ deleted }, "disk-pressure: post-sweep df parse failed; allowing write");
71
+ return;
72
+ }
73
+ const postProjectedMb = postFreeMb - incomingMb;
74
+ if (postProjectedMb >= this.opts.criticalFreeMb) {
75
+ args.log.info({ initialFreeMb, postFreeMb, deleted, postProjectedMb }, "disk-pressure: sweep recovered enough space");
76
+ return;
77
+ }
78
+ throw new ResolverError("disk_pressure", `device sandbox is critically low (${postFreeMb}MB free, threshold ${this.opts.criticalFreeMb}MB)`, {
79
+ initialFreeMb,
80
+ postFreeMb,
81
+ incomingMb,
82
+ sweptFiles: deleted,
83
+ sandboxDir: args.sandboxDir,
84
+ });
85
+ }
86
+ /**
87
+ * Returns free MB on the volume containing `sandboxDir`, or `null`
88
+ * if `df` output couldn't be parsed. Visible-for-test.
89
+ */
90
+ async dfFreeMb(mcp, sandboxDir, log) {
91
+ if (typeof mcp.executeCommand !== "function")
92
+ return null;
93
+ let stdout = "";
94
+ try {
95
+ const r = await mcp.executeCommand(`df -k ${shellQuote(sandboxDir)} 2>/dev/null`);
96
+ stdout = (r.stdout ?? "").trim();
97
+ }
98
+ catch (e) {
99
+ log.warn({ err: errString(e), sandboxDir }, "disk-pressure: df threw");
100
+ return null;
101
+ }
102
+ return parseDfFreeMb(stdout);
103
+ }
104
+ }
105
+ /**
106
+ * Parse a `df -k` output and return free MB on the relevant volume,
107
+ * or `null` if the output doesn't match any known shape.
108
+ *
109
+ * Android BusyBox `df -k /sdcard` emits something like:
110
+ *
111
+ * Filesystem 1K-blocks Used Available Use% Mounted on
112
+ * /dev/block/dm-25 116228032 74283456 41789120 64% /storage/emulated
113
+ *
114
+ * The "Available" column is the 4th whitespace-separated field. We
115
+ * walk lines from the bottom up and return the first row with four
116
+ * numeric-looking fields after the device name. Anything else
117
+ * returns `null` and the L3 guard fails soft.
118
+ *
119
+ * Visible-for-test (exported as a pure function so unit tests don't
120
+ * need an MCP stub).
121
+ */
122
+ export function parseDfFreeMb(out) {
123
+ if (!out)
124
+ return null;
125
+ const lines = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
126
+ if (lines.length < 2)
127
+ return null;
128
+ // Header sanity — we expect "Filesystem" to be a token on the first line.
129
+ if (!/Filesystem/i.test(lines[0]))
130
+ return null;
131
+ for (let i = lines.length - 1; i >= 1; i--) {
132
+ const cols = lines[i].split(/\s+/);
133
+ if (cols.length >= 4 && /^\d+$/.test(cols[3])) {
134
+ const availKb = parseInt(cols[3], 10);
135
+ return Math.floor(availKb / 1024);
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ function shellQuote(s) {
141
+ return `'${s.replace(/'/g, "'\\''")}'`;
142
+ }
143
+ function errString(e) {
144
+ if (e instanceof Error)
145
+ return e.message;
146
+ return String(e);
147
+ }
148
+ //# sourceMappingURL=disk-pressure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk-pressure.js","sourceRoot":"","sources":["../../../src/resources/lifecycle/disk-pressure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * resources/lifecycle/ — barrel for the three-layer GC.
3
+ */
4
+ export type { LifecycleMode, ResourceLifecycleConfig, ResourceLifecycleManager, ResourceLifecycleSweeper, LifecycleWiring, } from "./types.js";
5
+ export { FsResourceLifecycleManager } from "./manager.js";
6
+ export { FsResourceLifecycleSweeper } from "./sweeper.js";
7
+ export { FsDiskPressureGuard, parseDfFreeMb, type DiskPressureGuardOptions, } from "./disk-pressure.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * resources/lifecycle/ — barrel for the three-layer GC.
3
+ */
4
+ export { FsResourceLifecycleManager } from "./manager.js";
5
+ export { FsResourceLifecycleSweeper } from "./sweeper.js";
6
+ export { FsDiskPressureGuard, parseDfFreeMb, } from "./disk-pressure.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/resources/lifecycle/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EACL,mBAAmB,EACnB,aAAa,GAEd,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Resource Lifecycle — L1 (per-task ephemeral) implementation.
3
+ *
4
+ * Tracks every fresh asset a task pushed to the device and, when the
5
+ * task ends, schedules a delayed `rm` + cache invalidate + MediaScanner
6
+ * rebroadcast. The delay protects against asynchronous picker uploads
7
+ * that read the original file *after* the task completes (WeChat
8
+ * Moments, for example, queues a background upload and reads the file
9
+ * a second time when the user has slow connectivity).
10
+ *
11
+ * Cache hits are intentionally NOT tracked here. They were created by
12
+ * a prior task that owns their lifecycle; deleting them eagerly would
13
+ * starve a `cache hit + revalidate` workflow that the resolution layer
14
+ * already supports out of the box. They get reclaimed by L2 once
15
+ * `sweepAgeHours` elapses.
16
+ *
17
+ * The implementation is platform-aware: when `device.getInfo().os` is
18
+ * not `"android"`, every method short-circuits to a no-op so the desk-
19
+ * top adapter can boot without spurious telemetry.
20
+ */
21
+ import type { ResolvedAsset } from "../types.js";
22
+ import type { LifecycleWiring, ResourceLifecycleManager } from "./types.js";
23
+ export declare class FsResourceLifecycleManager implements ResourceLifecycleManager {
24
+ private readonly tasks;
25
+ private readonly wiring;
26
+ /**
27
+ * Cached platform string from `device.getInfo()`. We resolve lazily
28
+ * so unit tests don't have to await `getInfo()` at construction
29
+ * time. `null` = not yet probed; `""` = probed and unknown.
30
+ */
31
+ private platform;
32
+ constructor(wiring: LifecycleWiring);
33
+ trackFresh(taskId: string, asset: ResolvedAsset): void;
34
+ onTaskEnd(taskId: string): Promise<void>;
35
+ flushPending(): Promise<void>;
36
+ private detectPlatform;
37
+ private deletePaths;
38
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Resource Lifecycle — L1 (per-task ephemeral) implementation.
3
+ *
4
+ * Tracks every fresh asset a task pushed to the device and, when the
5
+ * task ends, schedules a delayed `rm` + cache invalidate + MediaScanner
6
+ * rebroadcast. The delay protects against asynchronous picker uploads
7
+ * that read the original file *after* the task completes (WeChat
8
+ * Moments, for example, queues a background upload and reads the file
9
+ * a second time when the user has slow connectivity).
10
+ *
11
+ * Cache hits are intentionally NOT tracked here. They were created by
12
+ * a prior task that owns their lifecycle; deleting them eagerly would
13
+ * starve a `cache hit + revalidate` workflow that the resolution layer
14
+ * already supports out of the box. They get reclaimed by L2 once
15
+ * `sweepAgeHours` elapses.
16
+ *
17
+ * The implementation is platform-aware: when `device.getInfo().os` is
18
+ * not `"android"`, every method short-circuits to a no-op so the desk-
19
+ * top adapter can boot without spurious telemetry.
20
+ */
21
+ import { ResourceResolutionCache } from "../cache.js";
22
+ export class FsResourceLifecycleManager {
23
+ tasks = new Map();
24
+ wiring;
25
+ /**
26
+ * Cached platform string from `device.getInfo()`. We resolve lazily
27
+ * so unit tests don't have to await `getInfo()` at construction
28
+ * time. `null` = not yet probed; `""` = probed and unknown.
29
+ */
30
+ platform = null;
31
+ constructor(wiring) {
32
+ this.wiring = wiring;
33
+ }
34
+ trackFresh(taskId, asset) {
35
+ if (this.wiring.config.mode === "off")
36
+ return;
37
+ let entry = this.tasks.get(taskId);
38
+ if (!entry) {
39
+ entry = { paths: new Set(), cacheKeys: new Set(), ended: false };
40
+ this.tasks.set(taskId, entry);
41
+ }
42
+ // AgentHost.runOne always calls trackFresh BEFORE onTaskEnd
43
+ // (resolution happens inside `resolveAndAugment`, the cleanup is
44
+ // wired in `runOne`'s `finally`). We rely on that invariant — any
45
+ // late arrival is a programmer error and fails loud naturally.
46
+ entry.paths.add(asset.remotePath);
47
+ if (asset.sourceUrl) {
48
+ entry.cacheKeys.add(ResourceResolutionCache.keyForUrl(asset.sourceUrl));
49
+ }
50
+ }
51
+ async onTaskEnd(taskId) {
52
+ const entry = this.tasks.get(taskId);
53
+ if (!entry)
54
+ return;
55
+ if (entry.ended) {
56
+ // Idempotency: a second onTaskEnd just observes the same
57
+ // memoised `fire()` promise. `fire` is unset for the
58
+ // persistent / no-op branches; in those cases there's nothing
59
+ // to wait for.
60
+ if (entry.fire)
61
+ await entry.fire();
62
+ return;
63
+ }
64
+ entry.ended = true;
65
+ const mode = this.wiring.config.mode;
66
+ if (mode !== "ephemeral") {
67
+ // persistent: keep the file, just drop our tracking; L2 will
68
+ // reap on age. (mode === "off" was already handled in
69
+ // trackFresh — entry.paths is empty.)
70
+ this.tasks.delete(taskId);
71
+ return;
72
+ }
73
+ if (entry.paths.size === 0) {
74
+ this.tasks.delete(taskId);
75
+ return;
76
+ }
77
+ const paths = Array.from(entry.paths);
78
+ const cacheKeys = Array.from(entry.cacheKeys);
79
+ const delaySec = Math.max(0, this.wiring.config.perTaskDeleteDelaySec);
80
+ // Single fire-once trigger shared by the timer, the idempotent
81
+ // second `onTaskEnd`, and the shutdown `flushPending` hijack.
82
+ // The first call kicks off `deletePaths(...)`; every subsequent
83
+ // call awaits the same Promise.
84
+ let firePromise = null;
85
+ const fire = () => {
86
+ if (!firePromise) {
87
+ firePromise = this.deletePaths(paths, cacheKeys).finally(() => {
88
+ this.tasks.delete(taskId);
89
+ });
90
+ }
91
+ return firePromise;
92
+ };
93
+ entry.fire = fire;
94
+ if (delaySec === 0) {
95
+ // Inline delete (tests + emergency shutdown).
96
+ await fire();
97
+ return;
98
+ }
99
+ // Schedule the delete; let the caller (`agent-host.runOne`)
100
+ // return as soon as cleanup is *scheduled*. The SIGTERM handler
101
+ // will call `flushPending` to fire the timer early if we shut
102
+ // down before the delay elapses.
103
+ entry.timer = setTimeout(() => {
104
+ void fire();
105
+ }, delaySec * 1000);
106
+ if (typeof entry.timer.unref === "function") {
107
+ entry.timer.unref();
108
+ }
109
+ }
110
+ async flushPending() {
111
+ const pending = [];
112
+ for (const [, entry] of this.tasks) {
113
+ if (entry.timer) {
114
+ clearTimeout(entry.timer);
115
+ entry.timer = undefined;
116
+ }
117
+ if (entry.fire)
118
+ pending.push(entry.fire());
119
+ }
120
+ this.tasks.clear();
121
+ await Promise.allSettled(pending);
122
+ }
123
+ // ── Internals ───────────────────────────────────────────────────────────
124
+ async detectPlatform() {
125
+ if (this.platform !== null)
126
+ return this.platform;
127
+ try {
128
+ const info = await this.wiring.device.getInfo();
129
+ this.platform = (info.os ?? info.type ?? "").toString().toLowerCase();
130
+ }
131
+ catch {
132
+ this.platform = "";
133
+ }
134
+ return this.platform;
135
+ }
136
+ async deletePaths(paths, cacheKeys) {
137
+ if (paths.length === 0)
138
+ return;
139
+ const platform = await this.detectPlatform();
140
+ if (platform !== "android") {
141
+ // Non-Android (desktop, lab) — nothing to do on disk; still
142
+ // invalidate cache so an eventual `validateAlive` re-check
143
+ // doesn't return a stale entry.
144
+ for (const k of cacheKeys)
145
+ this.wiring.cache?.invalidate(k);
146
+ return;
147
+ }
148
+ let mcp;
149
+ try {
150
+ mcp = this.wiring.mcpProvider();
151
+ }
152
+ catch (e) {
153
+ this.wiring.log.warn({ paths, err: errString(e) }, "lifecycle: mcpProvider() threw; skipping rm (cache still invalidated)");
154
+ for (const k of cacheKeys)
155
+ this.wiring.cache?.invalidate(k);
156
+ return;
157
+ }
158
+ if (typeof mcp.executeCommand !== "function") {
159
+ // No way to talk to the device shell. Best-effort: keep cache
160
+ // honest so the next resolve sees a clean slate.
161
+ for (const k of cacheKeys)
162
+ this.wiring.cache?.invalidate(k);
163
+ return;
164
+ }
165
+ // 1. rm -f every tracked file. We batch so a single `executeCommand`
166
+ // round-trip handles everything; on a multi-asset task that's
167
+ // one shell hop instead of N. The `||:` swallows partial
168
+ // failures (e.g. file already gone via L2) without poisoning
169
+ // the rest of the batch.
170
+ const rmCmd = paths.map((p) => `rm -f ${shellQuote(p)}`).join(" ; ") + " ; :";
171
+ try {
172
+ await mcp.executeCommand(rmCmd);
173
+ }
174
+ catch (e) {
175
+ this.wiring.log.warn({ paths, err: errString(e) }, "lifecycle: rm batch failed (non-fatal)");
176
+ }
177
+ // 2. Trigger MediaScanner so the gallery thumbnail disappears
178
+ // promptly. Per-file broadcast — the API only accepts a single
179
+ // `-d` URI per `am broadcast` invocation.
180
+ for (const p of paths) {
181
+ try {
182
+ await mcp.executeCommand(`am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file://${p}`);
183
+ }
184
+ catch (e) {
185
+ this.wiring.log.debug({ path: p, err: errString(e) }, "lifecycle: media-scan after rm failed (non-fatal)");
186
+ }
187
+ }
188
+ // 3. Drop cache entries so the next `submitTask` for the same
189
+ // URL re-resolves. `validateAlive` would catch this anyway via
190
+ // a follow-up `ls`, but invalidating now saves one shell
191
+ // round-trip on the happy path.
192
+ for (const k of cacheKeys)
193
+ this.wiring.cache?.invalidate(k);
194
+ this.wiring.log.info({ count: paths.length, paths }, "lifecycle: per-task cleanup complete");
195
+ }
196
+ }
197
+ function shellQuote(s) {
198
+ return `'${s.replace(/'/g, "'\\''")}'`;
199
+ }
200
+ function errString(e) {
201
+ if (e instanceof Error)
202
+ return e.message;
203
+ return String(e);
204
+ }
205
+ //# sourceMappingURL=manager.js.map