@zaly/agent 0.0.4 → 0.0.6

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 (53) hide show
  1. package/dist/{agent-GDLdDc2w.mjs → agent-DirSoZjM.mjs} +8 -9
  2. package/dist/agent-DirSoZjM.mjs.map +1 -0
  3. package/dist/{bash-QNMTQ1PD.mjs → bash-COgq4W7U.mjs} +2 -2
  4. package/dist/{bash-QNMTQ1PD.mjs.map → bash-COgq4W7U.mjs.map} +1 -1
  5. package/dist/compactions-3AjPqjeZ.mjs.map +1 -1
  6. package/dist/{fetch-D8cmmA3E.mjs → fetch-DXuxAg_t.mjs} +3 -3
  7. package/dist/{fetch-D8cmmA3E.mjs.map → fetch-DXuxAg_t.mjs.map} +1 -1
  8. package/dist/{grep-B2Ax7ewy.mjs → grep-B4GF0svc.mjs} +2 -2
  9. package/dist/{grep-B2Ax7ewy.mjs.map → grep-B4GF0svc.mjs.map} +1 -1
  10. package/dist/index-node-esm-DAnl9SaE.mjs +2114 -0
  11. package/dist/index-node-esm-DAnl9SaE.mjs.map +1 -0
  12. package/dist/index.d.mts +47 -42
  13. package/dist/index.d.mts.map +1 -1
  14. package/dist/index.mjs +28 -22
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/{jsonl-Dw_VBp46.mjs → jsonl-CvgEOakL.mjs} +2 -2
  17. package/dist/jsonl-CvgEOakL.mjs.map +1 -0
  18. package/dist/{manager-D-lWBTHG.mjs → manager-DHBfEtge.mjs} +12 -2
  19. package/dist/manager-DHBfEtge.mjs.map +1 -0
  20. package/dist/masker-Bv9KjkDF.mjs.map +1 -1
  21. package/dist/notify-DpG_m9iP.mjs.map +1 -1
  22. package/dist/{registry-CFmR5G3J.mjs → registry-Bao6L4Ue.mjs} +399 -73
  23. package/dist/registry-Bao6L4Ue.mjs.map +1 -0
  24. package/dist/session/claude.d.mts +2 -3
  25. package/dist/session/claude.d.mts.map +1 -1
  26. package/dist/session/claude.mjs +1 -1
  27. package/dist/{session-CUUdWFC5.mjs → session-B1lXSWhT.mjs} +4 -4
  28. package/dist/session-B1lXSWhT.mjs.map +1 -0
  29. package/dist/session.d.mts +8 -8
  30. package/dist/session.d.mts.map +1 -1
  31. package/dist/session.mjs +3 -3
  32. package/dist/{subagent-baqzjRMc.mjs → subagent-Zz-MQHk2.mjs} +3 -3
  33. package/dist/{subagent-baqzjRMc.mjs.map → subagent-Zz-MQHk2.mjs.map} +1 -1
  34. package/dist/swarm-BJ3gQA1Z.mjs.map +1 -1
  35. package/dist/{tasks-D8rARHZ1.mjs → tasks-BA-PCeGa.mjs} +4 -3
  36. package/dist/{tasks-D8rARHZ1.mjs.map → tasks-BA-PCeGa.mjs.map} +1 -1
  37. package/dist/{tasks-WmC270X2.mjs → tasks-WdRAHsOL.mjs} +5 -4
  38. package/dist/tasks-WdRAHsOL.mjs.map +1 -0
  39. package/dist/{tools-DW1dbcCz.mjs → tools-CZvBFp-v.mjs} +144 -38
  40. package/dist/tools-CZvBFp-v.mjs.map +1 -0
  41. package/dist/truncate-BSMXgiXj.mjs.map +1 -1
  42. package/dist/{uuid-ByXP5SO5.mjs → uuid-CAdJg4l8.mjs} +2 -2
  43. package/dist/{uuid-ByXP5SO5.mjs.map → uuid-CAdJg4l8.mjs.map} +1 -1
  44. package/package.json +12 -12
  45. package/dist/agent-GDLdDc2w.mjs.map +0 -1
  46. package/dist/index-node-esm-D6YQRKEz.mjs +0 -1537
  47. package/dist/index-node-esm-D6YQRKEz.mjs.map +0 -1
  48. package/dist/jsonl-Dw_VBp46.mjs.map +0 -1
  49. package/dist/manager-D-lWBTHG.mjs.map +0 -1
  50. package/dist/registry-CFmR5G3J.mjs.map +0 -1
  51. package/dist/session-CUUdWFC5.mjs.map +0 -1
  52. package/dist/tasks-WmC270X2.mjs.map +0 -1
  53. package/dist/tools-DW1dbcCz.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager-DHBfEtge.mjs","names":[],"sources":["../src/permissions/presets.ts","../src/permissions/manager.ts"],"sourcesContent":["import type { Verdict } from \"./types.ts\"\n\n/**\n * Permission presets — verdict-keyed pattern lists in the same shape\n * `parseRules` accepts. The TUI exposes these as `--preset strict`,\n * `--preset readonly`, etc.; the manager resolves the chosen preset to\n * the structured `Rule<string>[]` at construction.\n *\n * Pattern grammar:\n * \"bash(ls:*)\" → bash command pattern\n * \"read(/src/**)\" → workspace-relative gitignore pattern\n * \"read(*)\" → matches every read in any workspace\n * \"bash\" → bare scope, equivalent to `bash(*)` (match any cmd)\n *\n * Scope names are exact matches against the tool name they gate\n * (snake_case). Underscored scopes work too (`task_stop`, `task_poll`).\n */\nexport type PermissionPresetName = \"strict\" | \"readonly\" | \"permissive\" | \"yolo\"\n\nexport interface PermissionPreset {\n description: string\n rules: Partial<Record<Verdict, string[]>>\n}\n\n// ── Pattern groups ─────────────────────────────────────────────────────────\n\n/** Native tools that only inspect session state or workspace files. */\nconst READONLY_TOOLS = [\n \"tool(read)\",\n \"tool(grep)\",\n \"tool(find)\",\n \"tool(task_list)\",\n \"tool(task_poll)\",\n]\n\n/** Read-only utilities the model can run without surprises: file\n * inspection, search, metadata. None of these modify state on their own.\n * (`sed`/`awk` writes still gated by Write rules via the bash handler's\n * composition with the file handler.) */\nconst READONLY_BASH = [\n \"bash(ls:*)\",\n \"bash(cat:*)\",\n \"bash(head:*)\",\n \"bash(tail:*)\",\n \"bash(wc:*)\",\n \"bash(find:*)\",\n \"bash(grep:*)\",\n \"bash(rg:*)\",\n \"bash(fd:*)\",\n \"bash(sort:*)\",\n \"bash(uniq:*)\",\n \"bash(cut:*)\",\n \"bash(diff:*)\",\n \"bash(stat:*)\",\n \"bash(file:*)\",\n \"bash(realpath:*)\",\n \"bash(readlink:*)\",\n \"bash(basename:*)\",\n \"bash(dirname:*)\",\n \"bash(echo:*)\",\n \"bash(printf:*)\",\n \"bash(sed:*)\",\n \"bash(awk:*)\",\n \"bash(xxd:*)\",\n \"bash(hexdump:*)\",\n \"bash(od:*)\",\n \"bash(tree:*)\",\n \"bash(pwd)\",\n \"bash(true)\",\n \"bash(false)\",\n \"bash(which:*)\",\n \"bash(type:*)\",\n \"bash(command:*)\",\n \"bash(test:*)\",\n \"bash(cd:*)\",\n]\n\n/** Read-only git + GitHub CLI subcommands. */\nconst READONLY_GIT = [\n \"bash(git status:*)\",\n \"bash(git log:*)\",\n \"bash(git diff:*)\",\n \"bash(git show:*)\",\n \"bash(git show-ref:*)\",\n \"bash(git branch:*)\",\n \"bash(git remote:*)\",\n \"bash(git rev-parse:*)\",\n \"bash(git config --get:*)\",\n \"bash(gh issue list:*)\",\n \"bash(gh issue view:*)\",\n \"bash(gh pr list:*)\",\n \"bash(gh pr view:*)\",\n \"bash(gh repo view:*)\",\n]\n\n/** Common dev-workflow commands — build, test, typecheck. Excludes\n * package management (`npm install`, `bun add`) which can drag arbitrary\n * code in over the network. */\nconst DEV_BASIC = [\n \"bash(bun test:*)\",\n \"bash(bun test:node:*)\",\n \"bash(bun test:bun:*)\",\n \"bash(bun check:*)\",\n \"bash(bun run:*)\",\n \"bash(bun build:*)\",\n \"bash(bunx tsc:*)\",\n \"bash(bun tsc:*)\",\n \"bash(bun x tsc:*)\",\n \"bash(bunx vitest:*)\",\n \"bash(bun x vitest:*)\",\n \"bash(bunx oxlint:*)\",\n \"bash(bun x oxlint:*)\",\n \"bash(oxlint:*)\",\n]\n\n/** Hard denies — never auto-allowed, regardless of preset. */\nconst HARD_DENIES = [\"bash(sudo:*)\"]\n\n/** Broader rules unlocked by the permissive preset. Trades some safety\n * for speed — appropriate for frontier models with strong instruction\n * following. Includes ad-hoc bun/node evals and git mutations. */\nconst PERMISSIVE_EXTRAS = [\n \"bash(bun -e:*)\",\n \"bash(bun:*)\",\n \"bash(node:*)\",\n \"bash(node -e:*)\",\n \"bash(make:*)\",\n \"bash(mkdir:*)\",\n \"bash(cp:*)\",\n \"bash(mv:*)\",\n \"bash(touch:*)\",\n \"bash(ln:*)\",\n \"bash(chmod:*)\",\n \"bash(git add:*)\",\n \"bash(git commit:*)\",\n \"bash(git checkout:*)\",\n \"bash(git restore:*)\",\n \"bash(git stash:*)\",\n \"bash(git fetch:*)\",\n \"bash(git pull:*)\",\n \"bash(curl:*)\",\n \"bash(jq:*)\",\n \"bash(perl -e:*)\",\n \"bash(timeout:*)\",\n \"bash(for:*)\",\n \"bash(while:*)\",\n]\n\n// ── Presets ────────────────────────────────────────────────────────────────\n\n// oxlint-disable sort-keys\nexport const permissionPresets = {\n strict: {\n description: \"Ask for everything. No commands or file ops auto-allowed.\",\n rules: {\n deny: HARD_DENIES,\n // Override the tool/file handlers' default allows.\n ask: [\"tool(*)\", \"read(*)\", \"write(*)\"],\n },\n },\n readonly: {\n description:\n \"Auto-allow read-only utilities, git read-only ops, and common build/test commands. \" +\n \"Reads inside the workspace are allowed (sensitive paths denied); writes always ask.\",\n rules: {\n deny: HARD_DENIES,\n allow: [...READONLY_TOOLS, ...READONLY_BASH, ...READONLY_GIT, ...DEV_BASIC],\n ask: [\"tool(*)\", \"write(*)\"],\n },\n },\n permissive: {\n description:\n \"Readonly preset plus broader dev commands (file moves, ad-hoc bun/node evals, \" +\n \"git mutations, network fetches) and writes inside the workspace.\",\n rules: {\n deny: HARD_DENIES,\n allow: [\n \"tool(*)\",\n ...READONLY_BASH,\n ...READONLY_GIT,\n ...DEV_BASIC,\n ...PERMISSIVE_EXTRAS,\n // Allow writes inside any workspace (sensitive paths still denied\n // by the file handler before rule resolution).\n \"write(*)\",\n ],\n },\n },\n yolo: {\n description: \"Allow everything. No prompts. Use only when you trust the model and the task.\",\n rules: {\n allow: [\"bash\", \"read(*)\", \"write(*)\", \"tool(*)\"],\n },\n },\n} as const satisfies Record<PermissionPresetName, PermissionPreset>\n","import type { PermissionPresetName } from \"./presets.ts\"\nimport type { CheckResult, Rule, Verdict } from \"./types.ts\"\n\nimport { normPath } from \"@zaly/shared\"\nimport { handlerRegistry } from \"./handlers/registry.ts\"\nimport { permissionPresets } from \"./presets.ts\"\n\nexport type PermissionOptions = {\n /** Session cwd — anchor for resolving relative paths and the default\n * spawn cwd for bash subprocesses. Pinned at construction; the agent\n * never mutates `process.cwd()`, and there's no `cd` tool. Defaults\n * to `process.cwd()`. */\n cwd?: string\n /** Named preset — supplies a baseline rule set. Explicit `rules` win\n * over preset rules on conflict (first-match-wins resolution; user\n * rules are listed before preset rules at construction). */\n preset?: PermissionPresetName\n rules?: Rule[] | Partial<Record<Verdict, string[] | undefined>>\n /** Initial workspace list. `cwd` is auto-added so the user doesn't\n * have to repeat it. Pass paths absolute (or relative — they'll be\n * resolved against `cwd`). */\n workspaces?: readonly string[]\n}\n\n/**\n * Owns per-session permission state and dispatches `validate(scope, input)`\n * to the registered handler for `scope`. Handlers receive rules\n * pre-filtered to their scope plus the shared workspace list.\n *\n * Mutators (`addWorkspace`, `addRule`, …) are how the prompt UI promotes\n * an `ask` verdict — once the user picks \"allow `Bash(git push:*)` for\n * this session\", the TUI calls `manager.addRule(...)` and subsequent\n * `validate()` calls pick it up via the filter.\n */\nexport class PermissionManager {\n readonly cwd: string\n #rules: Rule[] = []\n #workspaces: string[]\n\n constructor(opts?: PermissionOptions) {\n this.cwd = normPath(opts?.cwd)\n if (opts?.rules) {\n const more = Array.isArray(opts.rules) ? opts.rules : parseRules(opts.rules)\n this.#rules.push(...more)\n }\n if (opts?.preset) this.#rules.push(...parseRules(permissionPresets[opts.preset].rules))\n this.#workspaces = (opts?.workspaces ?? []).map((p) => normPath(this.cwd, p))\n // Auto-include cwd so users don't have to repeat it. Listed first so\n // longest-prefix lookup in handlers stays predictable.\n if (!this.#workspaces.includes(this.cwd)) this.#workspaces.unshift(this.cwd)\n }\n\n // ── Workspaces ──────────────────────────────────────────────────────\n\n get workspaces(): readonly string[] {\n return this.#workspaces\n }\n\n addWorkspace(path: string): void {\n const abs = normPath(this.cwd, path)\n if (!this.#workspaces.includes(abs)) this.#workspaces.push(abs)\n }\n\n removeWorkspace(path: string): void {\n const abs = normPath(this.cwd, path)\n const i = this.#workspaces.indexOf(abs)\n if (i !== -1) this.#workspaces.splice(i, 1)\n }\n\n // ── Rules ───────────────────────────────────────────────────────────\n\n get rules(): readonly Rule[] {\n return this.#rules\n }\n\n /** Rules whose scope has no registered handler — surfaces config\n * errors (typos, unknown scopes, malformed pattern strings). The TUI\n * can render these as a startup warning. */\n get invalidRules(): readonly Rule[] {\n return this.#rules.filter((r) => !handlerRegistry.has(r.scope))\n }\n\n addRule(rule: Rule): void {\n this.#rules.unshift(rule)\n }\n\n // ── Dispatch ────────────────────────────────────────────────────────\n\n /** True only when a blanket allow has no earlier restrictive exceptions. */\n allowsAll(scope: string): boolean {\n for (const rule of this.#rules) {\n if (rule.scope !== scope) continue\n if (rule.policy !== \"allow\") return false\n if (rule.pattern === \"*\") return true\n }\n return false\n }\n\n validate(scope: string, input: string): CheckResult {\n if (!handlerRegistry.has(scope)) {\n throw new Error(`no permission handler registered for scope \"${scope}\"`)\n }\n const handler = handlerRegistry.load(scope)\n const rules = this.#rules.filter((r) => r.scope === scope)\n return handler.validate(input, {\n allowsAll: this.allowsAll.bind(this),\n cwd: this.cwd,\n rules,\n scope,\n validate: this.validate.bind(this),\n workspaces: this.#workspaces,\n })\n }\n}\n\n/** Parse Claude-Code-style permission config (a verdict-keyed map of\n * pattern strings) into structured `Rule<string>[]`.\n *\n * Pattern shapes accepted:\n * \"Bash(ls:*)\" → { scope: \"bash\", pattern: \"ls:*\" }\n * \"WebSearch\" → { scope: \"websearch\", pattern: \"*\" } // bare name = scope-wide\n *\n * Scopes are normalized to lower-case so config can use either\n * TitleCase (\"Bash\") or lowercase (\"bash\") interchangeably.\n *\n * Malformed entries are pushed verbatim so `manager.invalidRules`\n * surfaces them — typos in the scope (\"Bahs(...)\") are caught the same\n * way (no handler registered → flagged invalid). */\nexport function parseRules(rules: Partial<Record<Verdict, string[] | undefined>>): Rule[] {\n const ret: Rule[] = []\n for (const policy of Object.keys(rules) as Verdict[]) {\n const patterns = rules[policy]\n if (!Array.isArray(patterns)) continue\n for (const raw of patterns) {\n const m = raw.match(/^(\\w+)(?:\\((.+)\\))?$/)\n if (m) {\n const [, scope, pat = \"*\"] = m\n ret.push({ pattern: pat, policy, scope: scope.toLowerCase() })\n } else {\n // Truly malformed — preserve verbatim so invalidRules surfaces it.\n ret.push({ pattern: raw, policy, scope: \"invalid\" })\n }\n }\n }\n return ret\n}\n"],"mappings":";;;;AA2BA,MAAM,iBAAiB;CACrB;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,MAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,eAAe;CACnB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;AAKA,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,cAAc,CAAC,cAAc;;;;AAKnC,MAAM,oBAAoB;CACxB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAKA,MAAa,oBAAoB;CAC/B,QAAQ;EACN,aAAa;EACb,OAAO;GACL,MAAM;GAEN,KAAK;IAAC;IAAW;IAAW;GAAU;EACxC;CACF;CACA,UAAU;EACR,aACE;EAEF,OAAO;GACL,MAAM;GACN,OAAO;IAAC,GAAG;IAAgB,GAAG;IAAe,GAAG;IAAc,GAAG;GAAS;GAC1E,KAAK,CAAC,WAAW,UAAU;EAC7B;CACF;CACA,YAAY;EACV,aACE;EAEF,OAAO;GACL,MAAM;GACN,OAAO;IACL;IACA,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IAGH;GACF;EACF;CACF;CACA,MAAM;EACJ,aAAa;EACb,OAAO,EACL,OAAO;GAAC;GAAQ;GAAW;GAAY;EAAS,EAClD;CACF;AACF;;;;;;;;;;;;;AChKA,IAAa,oBAAb,MAA+B;CAC7B;CACA,SAAiB,CAAC;CAClB;CAEA,YAAY,MAA0B;EACpC,KAAK,MAAM,SAAS,MAAM,GAAG;EAC7B,IAAI,MAAM,OAAO;GACf,MAAM,OAAO,MAAM,QAAQ,KAAK,KAAK,IAAI,KAAK,QAAQ,WAAW,KAAK,KAAK;GAC3E,KAAK,OAAO,KAAK,GAAG,IAAI;EAC1B;EACA,IAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,GAAG,WAAW,kBAAkB,KAAK,OAAO,CAAC,KAAK,CAAC;EACtF,KAAK,eAAe,MAAM,cAAc,CAAC,EAAA,CAAG,KAAK,MAAM,SAAS,KAAK,KAAK,CAAC,CAAC;EAG5E,IAAI,CAAC,KAAK,YAAY,SAAS,KAAK,GAAG,GAAG,KAAK,YAAY,QAAQ,KAAK,GAAG;CAC7E;CAIA,IAAI,aAAgC;EAClC,OAAO,KAAK;CACd;CAEA,aAAa,MAAoB;EAC/B,MAAM,MAAM,SAAS,KAAK,KAAK,IAAI;EACnC,IAAI,CAAC,KAAK,YAAY,SAAS,GAAG,GAAG,KAAK,YAAY,KAAK,GAAG;CAChE;CAEA,gBAAgB,MAAoB;EAClC,MAAM,MAAM,SAAS,KAAK,KAAK,IAAI;EACnC,MAAM,IAAI,KAAK,YAAY,QAAQ,GAAG;EACtC,IAAI,MAAM,IAAI,KAAK,YAAY,OAAO,GAAG,CAAC;CAC5C;CAIA,IAAI,QAAyB;EAC3B,OAAO,KAAK;CACd;;;;CAKA,IAAI,eAAgC;EAClC,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC,gBAAgB,IAAI,EAAE,KAAK,CAAC;CAChE;CAEA,QAAQ,MAAkB;EACxB,KAAK,OAAO,QAAQ,IAAI;CAC1B;;CAKA,UAAU,OAAwB;EAChC,KAAK,MAAM,QAAQ,KAAK,QAAQ;GAC9B,IAAI,KAAK,UAAU,OAAO;GAC1B,IAAI,KAAK,WAAW,SAAS,OAAO;GACpC,IAAI,KAAK,YAAY,KAAK,OAAO;EACnC;EACA,OAAO;CACT;CAEA,SAAS,OAAe,OAA4B;EAClD,IAAI,CAAC,gBAAgB,IAAI,KAAK,GAC5B,MAAM,IAAI,MAAM,+CAA+C,MAAM,EAAE;EAEzE,MAAM,UAAU,gBAAgB,KAAK,KAAK;EAC1C,MAAM,QAAQ,KAAK,OAAO,QAAQ,MAAM,EAAE,UAAU,KAAK;EACzD,OAAO,QAAQ,SAAS,OAAO;GAC7B,WAAW,KAAK,UAAU,KAAK,IAAI;GACnC,KAAK,KAAK;GACV;GACA;GACA,UAAU,KAAK,SAAS,KAAK,IAAI;GACjC,YAAY,KAAK;EACnB,CAAC;CACH;AACF;;;;;;;;;;;;;;AAeA,SAAgB,WAAW,OAA+D;CACxF,MAAM,MAAc,CAAC;CACrB,KAAK,MAAM,UAAU,OAAO,KAAK,KAAK,GAAgB;EACpD,MAAM,WAAW,MAAM;EACvB,IAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;EAC9B,KAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,IAAI,IAAI,MAAM,sBAAsB;GAC1C,IAAI,GAAG;IACL,MAAM,GAAG,OAAO,MAAM,OAAO;IAC7B,IAAI,KAAK;KAAE,SAAS;KAAK;KAAQ,OAAO,MAAM,YAAY;IAAE,CAAC;GAC/D,OAEE,IAAI,KAAK;IAAE,SAAS;IAAK;IAAQ,OAAO;GAAU,CAAC;EAEvD;CACF;CACA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"masker-Bv9KjkDF.mjs","names":["#rule","defaults","#policies","#groups","#messages","#policy","#reset","#index","#o","#agent","#opts","#session","#masked","#stats","#threshold","#maskOpts","#update","#mask","#stat"],"sources":["../src/context/scoring.ts","../src/masker.ts"],"sourcesContent":["// ── Frecency tuning ───────────────────────────────────────────────────\n// Half-life measured in assistant turns, not wall time — what matters for\n// compaction context is what's been hot in the agent's *recent\n// experience*, regardless of how long the user spent between turns.\n// 60 turns is a middle ground: actively-iterated work stays at the top\n// while recurring workflow patterns (git stash, test-runner flags) still\n// clear the minScore filter instead of getting buried under one-shot\n\nimport type {\n AnyPart,\n Message,\n MetaPart,\n ParamsOf,\n Role,\n SafeParamsOf,\n Tool,\n ToolCallPart,\n ToolResultPart,\n} from \"@zaly/ai\"\nimport type { BashTool } from \"../tools/bash.ts\"\nimport type { EditTool, EditToolMeta } from \"../tools/edit.ts\"\nimport type { ReadTool, ReadToolMeta } from \"../tools/read.ts\"\nimport type { AnyTool } from \"../tools/registry.ts\"\nimport type { WriteTool, WriteToolMeta } from \"../tools/write.ts\"\n\nimport { isAttachment, safeParseToolParams } from \"@zaly/ai\"\nimport { safeStringify } from \"@zaly/shared\"\n\nconst DEFAULT_HALF_LIFE = 40\nconst DEFAULT_WEIGHT = 1\nconst DEFAULT_GAMMA = 0.5\n\nconst maskedMeta = (result = \"result\", action = \"call\"): MetaPart => ({\n content: `Masked ${result}. Re-${action} to refresh`,\n tag: \"masked\",\n type: \"meta\",\n})\n\nexport type MsgPart<R extends Role = Role, P extends AnyPart = AnyPart> = {\n message: Message<R>\n part: P\n score: number\n turn: number\n $m: number\n $p: number\n}\n\nexport type MaskGroup<P extends MsgPart = MsgPart> = {\n id: string\n key: string\n policy: MaskPolicy<P, MaskGroup<P>>\n parts: P[]\n}\n\nexport type MaskRule<P extends MsgPart = MsgPart, G extends MaskGroup<P> = MaskGroup<P>> = {\n filter?: (part: MsgPart) => boolean\n id?: (part: P) => string | undefined\n update?: (group: G, part: P) => void\n gamma?: number | ((group: G) => number | undefined)\n halfLife?: number | ((group: G) => number | undefined)\n key?: \"id\" | ((group: G) => string | undefined)\n weight?: number | ((group: G) => number | undefined)\n mask: (part: P, group: G) => AnyPart\n}\n\nexport type ToolPart<T extends Tool = Tool, M extends object = object> = MsgPart<\n \"assistant\" | \"tool\" | \"system\",\n ToolCallPart<T[\"name\"], ParamsOf<T>> | ToolResultPart<T[\"name\"], M>\n>\n\nexport type ToolGroup<T extends Tool = Tool, M extends object = object> = MaskGroup<\n ToolPart<T, M>\n> & {\n name: T[\"name\"]\n params: SafeParamsOf<T>\n call?: ToolCallPart<T[\"name\"], ParamsOf<T>>\n result?: ToolResultPart<T[\"name\"], M>\n}\n\nexport type ToolRule<T extends Tool = Tool, M extends object = object> =\n | MaskRule<ToolPart<T, M>, ToolGroup<T, M>>\n | {\n key?: \"name\" | \"params\" | \"id\" | ((tool: ToolGroup<T, M>) => string | undefined)\n }\n\nclass MaskPolicy<\n P extends MsgPart = MsgPart,\n G extends MaskGroup<P> = MaskGroup<P>,\n> implements Required<MaskRule<P, G>> {\n #rule: MaskRule<P, G>\n\n constructor(rule: MaskRule<P, G>) {\n this.#rule = rule\n }\n\n update(group: G, part: P): void {\n group.parts.push(part)\n this.#rule.update?.(group, part)\n }\n\n mask(part: P, group: G): AnyPart {\n return this.#rule.mask(part, group)\n }\n\n filter(part: MsgPart): boolean {\n return this.#rule.filter?.(part) ?? true\n }\n\n id(part: P): string {\n return this.#rule.id?.(part) ?? `${part.$m}:${part.$p}`\n }\n\n halfLife(group: G): number {\n const halfLife = this.#rule.halfLife ?? DEFAULT_HALF_LIFE\n return typeof halfLife === \"number\" ? halfLife : (halfLife(group) ?? DEFAULT_HALF_LIFE)\n }\n\n lambda(group: G): number {\n return Math.LN2 / this.halfLife(group)\n }\n\n weight(group: G): number {\n const weight = this.#rule.weight ?? DEFAULT_WEIGHT\n return typeof weight === \"number\" ? weight : (weight(group) ?? DEFAULT_WEIGHT)\n }\n\n gamma(group: G): number {\n const gamma = this.#rule.gamma ?? DEFAULT_GAMMA\n return typeof gamma === \"number\" ? gamma : (gamma(group) ?? DEFAULT_GAMMA)\n }\n\n key(group: G): string {\n const key = this.#rule.key ?? \"id\"\n if (key === \"id\") return group.id\n else if (typeof key === \"function\") return key(group) ?? group.id\n return group.id\n }\n}\n\nclass ToolPolicy<T extends Tool = Tool, M extends object = object> extends MaskPolicy<\n ToolPart<T, M>,\n ToolGroup<T, M>\n> {\n constructor(name: string, rule: ToolRule<T, M>) {\n super({\n filter: (part) =>\n (part.part.type === \"tool-call\" || part.part.type === \"tool-result\") &&\n (part.part.name === name || name === \"*\"),\n id: (part) => part.part.id,\n mask: (part, group) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: truncate(safeStringify(group.params), 100) },\n }\n return { ...part.part, content: [maskedMeta()] }\n },\n update: (group, part) => {\n group.name = name\n if (part.part.type === \"tool-call\") {\n group.call = part.part\n group.params = safeParseToolParams(part.part.params)\n }\n if (part.part.type === \"tool-result\") group.result = part.part\n },\n ...rule,\n key: (group) => {\n const key = rule.key ?? \"params\"\n if (key === \"name\") return group.name\n if (key === \"params\" && group.params !== undefined) return safeStringify(group.params)\n if (key === \"id\") return group.id\n if (typeof key === \"function\") return key(group)\n return group.id\n },\n })\n }\n}\n\nfunction truncate(text: string, len: number): string {\n return text.length <= len ? text : `${text.slice(0, len)}…`\n}\n\nconst fileScore: ToolRule<\n ReadTool | WriteTool | EditTool,\n ReadToolMeta | WriteToolMeta | EditToolMeta\n> = {\n gamma: (t) => {\n if (t.name === \"write\") return 0.45\n if (t.name === \"edit\") return 0.65\n if (t.result?.meta?.full) return 0.75\n return 0.95\n },\n halfLife: (t) => {\n if (t.name === \"write\") return 90\n if (t.name === \"edit\") return 75\n if (t.result?.meta?.full) return 60\n return 50 // partial read\n },\n key: (t) => `file:${t.result?.meta?.path ?? t.id}`,\n mask: (part) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: true, path: part.part.params.path },\n }\n return part.part.isError ? part.part : { ...part.part, content: [maskedMeta()] }\n },\n weight: (t) => {\n if (t.name === \"write\") return 1.25\n if (t.name === \"edit\") return 1.2\n if (t.result?.meta?.full) return 1.1\n return 1\n },\n}\n\nexport type ContextScoringOptions = {\n tools?: Record<AnyTool | \"*\", ToolRule>\n parts?: MaskRule[]\n}\n\nconst defaults: ContextScoringOptions = {\n parts: [\n {\n filter: (part) => isAttachment(part.part),\n mask: (part) => maskedMeta(part.part.type, \"attach\"),\n },\n {\n filter: (part) => part.message.role === \"system\" && part.message.meta?.kind === \"task\",\n halfLife: 25,\n mask: (part) => {\n if (part.part.type === \"meta\") return part.part\n return maskedMeta(\"task\")\n },\n },\n ],\n // oxlint-disable-next-line sort-keys\n tools: {\n bash: {\n gamma: 0.35,\n halfLife: 25,\n key: (t: ToolGroup<BashTool>) => `bash:${t.params?.command?.replace(/\\s+/g, \" \") ?? t.id}`,\n },\n find: { gamma: 0.7, halfLife: 35 },\n grep: { gamma: 0.7, halfLife: 35 },\n search: { gamma: 0.5, halfLife: 40 },\n\n read: fileScore,\n edit: fileScore,\n write: fileScore,\n\n \"*\": { gamma: 0.5, halfLife: 40 },\n } as Record<AnyTool | \"*\", ToolRule>,\n}\n\nexport class ContextScoring {\n #groups = new Map<string, MaskGroup>()\n #messages: readonly Message[] = []\n #policies: MaskPolicy[] = []\n\n constructor(opts: ContextScoringOptions = {}) {\n const tools = { ...defaults.tools, ...opts.tools }\n for (const [name, tool] of Object.entries(tools)) {\n this.#policies.push(new ToolPolicy(name, tool) as MaskPolicy)\n }\n for (const rule of [...(defaults.parts ?? []), ...(opts.parts ?? [])]) {\n this.#policies.push(new MaskPolicy(rule))\n }\n }\n\n #reset() {\n this.#groups.clear()\n }\n\n #index() {\n let turn = 0\n for (let $m = this.#messages.length - 1; $m >= 0; $m--) {\n const m = this.#messages[$m]\n if (m.role === \"assistant\") turn++\n\n if (!Array.isArray(m.content)) continue\n\n for (let $p = m.content.length - 1; $p >= 0; $p--) {\n const part: MsgPart = { $m, $p, message: m, part: m.content[$p], score: 0, turn }\n const policy = this.#policy(part)\n if (!policy) continue\n const id = policy.id(part)\n const group = this.#groups.get(id) ?? { id, key: \"\", parts: [], policy }\n policy.update(group, part)\n this.#groups.set(id, group)\n }\n }\n }\n\n #policy(part: MsgPart): MaskPolicy | undefined {\n return this.#policies.find((policy) => policy.filter(part))\n }\n\n score(messages: readonly Message[]): MaskGroup[] {\n this.#reset()\n this.#messages = messages\n this.#index()\n // tools sorted by recency\n const groups = [...this.#groups.values()]\n const gammas = new Map<string, number>()\n for (const group of groups) {\n const key = group.policy.key(group)\n group.key = key\n const weight = group.policy.weight(group)\n const shadow = gammas.get(key) ?? 1\n const lambda = group.policy.lambda(group)\n for (const part of group.parts) {\n const decay = Math.exp(-lambda * part.turn)\n part.score = decay * weight * shadow\n }\n gammas.set(key, shadow * group.policy.gamma(group))\n }\n return groups\n }\n}\n","import type { AnyPart, Message, Role, Tool } from \"@zaly/ai\"\nimport type { MaybeGetter } from \"@zaly/shared\"\nimport type { Agent } from \"./agent.ts\"\nimport type { MsgPart } from \"./context/scoring.ts\"\nimport type { Session } from \"./session/session.ts\"\n\nimport { toValue } from \"@zaly/shared\"\nimport { ContextScoring } from \"./context/scoring.ts\"\nimport { estimatePart, tokenStats } from \"./context/tokens.ts\"\n\n/** Top-level masking config. */\nexport type MaskerOptions = {\n enabled?: boolean\n /** Don't mask tool-result parts whose original content is shorter\n * than this (estimated tokens). Skips tiny \"ok\"-style success\n * messages where the stub would be larger than the original.\n * Doesn't apply to attachments (always worth masking). */\n minTokens?: number\n /** How many turns to keep in the tail of the conversation, regardless\n * of score. Defaults to 20. */\n keepTurns?: number\n /** How far above the target ratio to trigger a new masking pass.\n * Defaults to 0.25 (25%). */\n delta?: number\n /** Target ratio of used/limit tokens to reach by masking. Defaults to 0.5 (50%). */\n target?: number\n}\n\nexport type MaskOpts = {\n force?: boolean\n limit?: number\n ratio?: number\n tools?: Tool[]\n prompt?: string[]\n}\n\nconst defaults = {\n delta: 0.25,\n enabled: true,\n keepTurns: 40,\n minTokens: 50,\n target: 0.5,\n} as const satisfies Required<MaskerOptions>\n\n/** In-place mask projection for the request stream.\n *\n * The masker replaces low-value parts with stable stubs to keep the\n * projected request within budget. A scoring pass ranks maskable parts\n * by recency, repeated-use shadowing, and per-policy weights; the budget\n * pass masks from lowest score upward until the target is reached or no\n * useful candidates remain.\n *\n * Masking intentionally happens rarely because it changes historical\n * bytes and busts prefix cache. The first pass after startup/session\n * reset always rebuilds the projection. Later passes use a hysteresis\n * threshold: mask toward `target`, then trigger again at roughly\n * `actual + delta`. If the target cannot be reached, the next threshold\n * rises automatically to avoid repeated no-op cache busts.\n */\nexport class Masker {\n #o: MaybeGetter<MaskerOptions>\n #stats = new Map<Role, Record<string, number>>()\n #masked = new Map<string, Map<number, AnyPart>>()\n #threshold?: number\n #agent: Agent\n\n constructor(agent: Agent, opts: MaybeGetter<MaskerOptions> = {}) {\n this.#o = opts\n this.#agent = agent\n agent.on(\"context\", async (ctx) => (ctx.messages = await this.mask(ctx.messages)))\n agent.ctx.on(\"session\", () => this.reset())\n }\n\n get #opts(): Required<MaskerOptions> {\n return { ...defaults, ...toValue(this.#o) }\n }\n\n get enabled(): boolean {\n return this.#opts.enabled\n }\n\n get #session(): Session {\n return this.#agent.session\n }\n\n #maskOpts(opts?: MaskOpts): Required<MaskOpts> {\n const pressure = this.#agent.pressure\n return {\n force: false,\n limit: pressure.limit,\n prompt: this.#agent.prompt,\n ratio: pressure.ratio,\n tools: this.#agent.tools,\n ...opts,\n }\n }\n\n reset(): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = undefined\n }\n\n get stats(): Map<Role, Record<string, number>> {\n return this.#stats\n }\n\n /** Number of messages with at least one masked part. */\n get masked(): number {\n return this.#masked.size\n }\n\n isMasked(msgId: string, partIdx?: number): boolean {\n if (!this.enabled) return false\n const parts = this.#masked.get(msgId)\n if (parts === undefined) return false\n return partIdx === undefined ? parts.size > 0 : parts.has(partIdx)\n }\n\n /** Apply masks and return the projected message array.\n *\n * Always rebuild once after startup/session reset: `pressure.ratio`\n * may describe a previously masked projection, while this instance has\n * no mask decisions yet. After that, rebuild only when projected\n * pressure crosses the hysteresis threshold. */\n async mask(messages: readonly Message[], opts?: MaskOpts): Promise<Message[]> {\n if (!this.enabled || messages.length === 0) return [...messages]\n\n const resolved = this.#maskOpts(opts)\n\n // Masker has not yet been initialized for this session.\n // Use the session's mask checkpoint to restore the last threshold\n // and rebuild the mask decisions up to that point.\n if (this.#threshold === undefined) {\n const cp = this.#session.maskCheckpoint\n if (!cp) {\n // No checkpoint means the session has never been masked before, so\n // just set to the default threshold for the first pass, so that the next pass will\n // trigger only if the pressure exceeds the target + delta.\n this.#threshold = this.#opts.target + this.#opts.delta\n } else {\n this.#threshold = cp.threshold\n const idx = messages.findIndex((m) => m.id === cp.messageId)\n // NOTE: the checkpoint message should exists. If not mask all, just in case\n // Rebuild the mask decisions\n this.#update(idx === -1 ? messages : messages.slice(0, idx + 1), resolved)\n }\n }\n\n // If the current pressure ratio exceeds the threshold, rebuild the mask decisions.\n if (resolved.ratio >= this.#threshold || resolved.force) {\n const messageId = messages.at(-1)?.id\n if (!messageId) throw new Error(\"Message in masker without ID. Should never happen.\")\n // Create a checkpoint first, with the CURRENT threshold, not the new one.\n await this.#session.addMaskCheckpoint({\n messageId,\n threshold: this.#threshold,\n })\n this.#update(messages, resolved)\n }\n return this.#mask(messages)\n }\n\n #stat(role: Role, key: string, n = 1): void {\n const r = this.#stats.get(role) ?? {}\n r[key] = (r[key] ?? 0) + n\n this.#stats.set(role, r)\n }\n\n /** Recompute mask decisions for the current message history. */\n #update(messages: readonly Message[], opts: Required<MaskOpts>): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = this.#opts.target + this.#opts.delta\n const usage = tokenStats(messages, { prompt: opts.prompt, tools: opts.tools })\n // `pressure.ratio` is based on the current projected/masked request,\n // not the raw session. Raw history may exceed model context; masking\n // only needs to keep the projected request in a safe band.\n const targetRatio = Math.max(this.#opts.target, this.#threshold - this.#opts.delta)\n const target = opts.limit * targetRatio\n\n // Return when already under target.\n if (usage.tokens <= target) return\n\n let mask = usage.tokens - target\n\n if (mask === 0) return\n\n const scorer = new ContextScoring()\n const scores = scorer.score(messages)\n const parts: (MsgPart & { mask: () => AnyPart })[] = []\n for (const s of scores) {\n for (const p of s.parts) {\n parts.push({\n ...p,\n mask: () => s.policy.mask(p, s),\n })\n }\n }\n parts.sort((a, b) => a.score - b.score)\n\n let masked = 0\n for (const p of parts) {\n if (mask <= 0) break\n const id = p.message.id\n if (!id) continue\n\n if (p.turn <= this.#opts.keepTurns) continue\n\n const tokens = estimatePart(p.part).tokens\n if (tokens < this.#opts.minTokens) continue\n\n const part = p.mask()\n if (part === p.part) continue\n\n const msgParts = this.#masked.get(id) ?? new Map()\n msgParts.set(p.$p, part)\n this.#stat(\n p.message.role,\n `${p.part.type}${p.part.type === \"tool-result\" || p.part.type === \"tool-call\" ? `:${p.part.name}` : \"\"}`\n )\n this.#masked.set(id, msgParts)\n const delta = tokens - estimatePart(part).tokens\n masked += delta\n mask -= delta\n }\n\n this.#threshold = Math.max(\n (usage.tokens - masked) / opts.limit + this.#opts.delta,\n this.#opts.target + this.#opts.delta\n )\n }\n\n /** Render the current projection using the last computed mask decisions. */\n #mask(messages: readonly Message[]): Message[] {\n return messages.map((m) => {\n if (m.id === undefined || typeof m.content === \"string\") return m\n const masked = m.id ? this.#masked.get(m.id) : undefined\n return masked && masked.size > 0\n ? ({\n ...m,\n content: m.content.map((p, i) => masked.get(i) ?? p),\n } as Message)\n : m\n })\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"masker-Bv9KjkDF.mjs","names":["defaults"],"sources":["../src/context/scoring.ts","../src/masker.ts"],"sourcesContent":["// ── Frecency tuning ───────────────────────────────────────────────────\n// Half-life measured in assistant turns, not wall time — what matters for\n// compaction context is what's been hot in the agent's *recent\n// experience*, regardless of how long the user spent between turns.\n// 60 turns is a middle ground: actively-iterated work stays at the top\n// while recurring workflow patterns (git stash, test-runner flags) still\n// clear the minScore filter instead of getting buried under one-shot\n\nimport type {\n AnyPart,\n Message,\n MetaPart,\n ParamsOf,\n Role,\n SafeParamsOf,\n Tool,\n ToolCallPart,\n ToolResultPart,\n} from \"@zaly/ai\"\nimport type { BashTool } from \"../tools/bash.ts\"\nimport type { EditTool, EditToolMeta } from \"../tools/edit.ts\"\nimport type { ReadTool, ReadToolMeta } from \"../tools/read.ts\"\nimport type { AnyTool } from \"../tools/registry.ts\"\nimport type { WriteTool, WriteToolMeta } from \"../tools/write.ts\"\n\nimport { isAttachment, safeParseToolParams } from \"@zaly/ai\"\nimport { safeStringify } from \"@zaly/shared\"\n\nconst DEFAULT_HALF_LIFE = 40\nconst DEFAULT_WEIGHT = 1\nconst DEFAULT_GAMMA = 0.5\n\nconst maskedMeta = (result = \"result\", action = \"call\"): MetaPart => ({\n content: `Masked ${result}. Re-${action} to refresh`,\n tag: \"masked\",\n type: \"meta\",\n})\n\nexport type MsgPart<R extends Role = Role, P extends AnyPart = AnyPart> = {\n message: Message<R>\n part: P\n score: number\n turn: number\n $m: number\n $p: number\n}\n\nexport type MaskGroup<P extends MsgPart = MsgPart> = {\n id: string\n key: string\n policy: MaskPolicy<P, MaskGroup<P>>\n parts: P[]\n}\n\nexport type MaskRule<P extends MsgPart = MsgPart, G extends MaskGroup<P> = MaskGroup<P>> = {\n filter?: (part: MsgPart) => boolean\n id?: (part: P) => string | undefined\n update?: (group: G, part: P) => void\n gamma?: number | ((group: G) => number | undefined)\n halfLife?: number | ((group: G) => number | undefined)\n key?: \"id\" | ((group: G) => string | undefined)\n weight?: number | ((group: G) => number | undefined)\n mask: (part: P, group: G) => AnyPart\n}\n\nexport type ToolPart<T extends Tool = Tool, M extends object = object> = MsgPart<\n \"assistant\" | \"tool\" | \"system\",\n ToolCallPart<T[\"name\"], ParamsOf<T>> | ToolResultPart<T[\"name\"], M>\n>\n\nexport type ToolGroup<T extends Tool = Tool, M extends object = object> = MaskGroup<\n ToolPart<T, M>\n> & {\n name: T[\"name\"]\n params: SafeParamsOf<T>\n call?: ToolCallPart<T[\"name\"], ParamsOf<T>>\n result?: ToolResultPart<T[\"name\"], M>\n}\n\nexport type ToolRule<T extends Tool = Tool, M extends object = object> =\n | MaskRule<ToolPart<T, M>, ToolGroup<T, M>>\n | {\n key?: \"name\" | \"params\" | \"id\" | ((tool: ToolGroup<T, M>) => string | undefined)\n }\n\nclass MaskPolicy<\n P extends MsgPart = MsgPart,\n G extends MaskGroup<P> = MaskGroup<P>,\n> implements Required<MaskRule<P, G>> {\n #rule: MaskRule<P, G>\n\n constructor(rule: MaskRule<P, G>) {\n this.#rule = rule\n }\n\n update(group: G, part: P): void {\n group.parts.push(part)\n this.#rule.update?.(group, part)\n }\n\n mask(part: P, group: G): AnyPart {\n return this.#rule.mask(part, group)\n }\n\n filter(part: MsgPart): boolean {\n return this.#rule.filter?.(part) ?? true\n }\n\n id(part: P): string {\n return this.#rule.id?.(part) ?? `${part.$m}:${part.$p}`\n }\n\n halfLife(group: G): number {\n const halfLife = this.#rule.halfLife ?? DEFAULT_HALF_LIFE\n return typeof halfLife === \"number\" ? halfLife : (halfLife(group) ?? DEFAULT_HALF_LIFE)\n }\n\n lambda(group: G): number {\n return Math.LN2 / this.halfLife(group)\n }\n\n weight(group: G): number {\n const weight = this.#rule.weight ?? DEFAULT_WEIGHT\n return typeof weight === \"number\" ? weight : (weight(group) ?? DEFAULT_WEIGHT)\n }\n\n gamma(group: G): number {\n const gamma = this.#rule.gamma ?? DEFAULT_GAMMA\n return typeof gamma === \"number\" ? gamma : (gamma(group) ?? DEFAULT_GAMMA)\n }\n\n key(group: G): string {\n const key = this.#rule.key ?? \"id\"\n if (key === \"id\") return group.id\n else if (typeof key === \"function\") return key(group) ?? group.id\n return group.id\n }\n}\n\nclass ToolPolicy<T extends Tool = Tool, M extends object = object> extends MaskPolicy<\n ToolPart<T, M>,\n ToolGroup<T, M>\n> {\n constructor(name: string, rule: ToolRule<T, M>) {\n super({\n filter: (part) =>\n (part.part.type === \"tool-call\" || part.part.type === \"tool-result\") &&\n (part.part.name === name || name === \"*\"),\n id: (part) => part.part.id,\n mask: (part, group) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: truncate(safeStringify(group.params), 100) },\n }\n return { ...part.part, content: [maskedMeta()] }\n },\n update: (group, part) => {\n group.name = name\n if (part.part.type === \"tool-call\") {\n group.call = part.part\n group.params = safeParseToolParams(part.part.params)\n }\n if (part.part.type === \"tool-result\") group.result = part.part\n },\n ...rule,\n key: (group) => {\n const key = rule.key ?? \"params\"\n if (key === \"name\") return group.name\n if (key === \"params\" && group.params !== undefined) return safeStringify(group.params)\n if (key === \"id\") return group.id\n if (typeof key === \"function\") return key(group)\n return group.id\n },\n })\n }\n}\n\nfunction truncate(text: string, len: number): string {\n return text.length <= len ? text : `${text.slice(0, len)}…`\n}\n\nconst fileScore: ToolRule<\n ReadTool | WriteTool | EditTool,\n ReadToolMeta | WriteToolMeta | EditToolMeta\n> = {\n gamma: (t) => {\n if (t.name === \"write\") return 0.45\n if (t.name === \"edit\") return 0.65\n if (t.result?.meta?.full) return 0.75\n return 0.95\n },\n halfLife: (t) => {\n if (t.name === \"write\") return 90\n if (t.name === \"edit\") return 75\n if (t.result?.meta?.full) return 60\n return 50 // partial read\n },\n key: (t) => `file:${t.result?.meta?.path ?? t.id}`,\n mask: (part) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: true, path: part.part.params.path },\n }\n return part.part.isError ? part.part : { ...part.part, content: [maskedMeta()] }\n },\n weight: (t) => {\n if (t.name === \"write\") return 1.25\n if (t.name === \"edit\") return 1.2\n if (t.result?.meta?.full) return 1.1\n return 1\n },\n}\n\nexport type ContextScoringOptions = {\n tools?: Record<AnyTool | \"*\", ToolRule>\n parts?: MaskRule[]\n}\n\nconst defaults: ContextScoringOptions = {\n parts: [\n {\n filter: (part) => isAttachment(part.part),\n mask: (part) => maskedMeta(part.part.type, \"attach\"),\n },\n {\n filter: (part) => part.message.role === \"system\" && part.message.meta?.kind === \"task\",\n halfLife: 25,\n mask: (part) => {\n if (part.part.type === \"meta\") return part.part\n return maskedMeta(\"task\")\n },\n },\n ],\n // oxlint-disable-next-line sort-keys\n tools: {\n bash: {\n gamma: 0.35,\n halfLife: 25,\n key: (t: ToolGroup<BashTool>) => `bash:${t.params?.command?.replace(/\\s+/g, \" \") ?? t.id}`,\n },\n find: { gamma: 0.7, halfLife: 35 },\n grep: { gamma: 0.7, halfLife: 35 },\n search: { gamma: 0.5, halfLife: 40 },\n\n read: fileScore,\n edit: fileScore,\n write: fileScore,\n\n \"*\": { gamma: 0.5, halfLife: 40 },\n } as Record<AnyTool | \"*\", ToolRule>,\n}\n\nexport class ContextScoring {\n #groups = new Map<string, MaskGroup>()\n #messages: readonly Message[] = []\n #policies: MaskPolicy[] = []\n\n constructor(opts: ContextScoringOptions = {}) {\n const tools = { ...defaults.tools, ...opts.tools }\n for (const [name, tool] of Object.entries(tools)) {\n this.#policies.push(new ToolPolicy(name, tool) as MaskPolicy)\n }\n for (const rule of [...(defaults.parts ?? []), ...(opts.parts ?? [])]) {\n this.#policies.push(new MaskPolicy(rule))\n }\n }\n\n #reset() {\n this.#groups.clear()\n }\n\n #index() {\n let turn = 0\n for (let $m = this.#messages.length - 1; $m >= 0; $m--) {\n const m = this.#messages[$m]\n if (m.role === \"assistant\") turn++\n\n if (!Array.isArray(m.content)) continue\n\n for (let $p = m.content.length - 1; $p >= 0; $p--) {\n const part: MsgPart = { $m, $p, message: m, part: m.content[$p], score: 0, turn }\n const policy = this.#policy(part)\n if (!policy) continue\n const id = policy.id(part)\n const group = this.#groups.get(id) ?? { id, key: \"\", parts: [], policy }\n policy.update(group, part)\n this.#groups.set(id, group)\n }\n }\n }\n\n #policy(part: MsgPart): MaskPolicy | undefined {\n return this.#policies.find((policy) => policy.filter(part))\n }\n\n score(messages: readonly Message[]): MaskGroup[] {\n this.#reset()\n this.#messages = messages\n this.#index()\n // tools sorted by recency\n const groups = [...this.#groups.values()]\n const gammas = new Map<string, number>()\n for (const group of groups) {\n const key = group.policy.key(group)\n group.key = key\n const weight = group.policy.weight(group)\n const shadow = gammas.get(key) ?? 1\n const lambda = group.policy.lambda(group)\n for (const part of group.parts) {\n const decay = Math.exp(-lambda * part.turn)\n part.score = decay * weight * shadow\n }\n gammas.set(key, shadow * group.policy.gamma(group))\n }\n return groups\n }\n}\n","import type { AnyPart, Message, Role, Tool } from \"@zaly/ai\"\nimport type { MaybeGetter } from \"@zaly/shared\"\nimport type { Agent } from \"./agent.ts\"\nimport type { MsgPart } from \"./context/scoring.ts\"\nimport type { Session } from \"./session/session.ts\"\n\nimport { toValue } from \"@zaly/shared\"\nimport { ContextScoring } from \"./context/scoring.ts\"\nimport { estimatePart, tokenStats } from \"./context/tokens.ts\"\n\n/** Top-level masking config. */\nexport type MaskerOptions = {\n enabled?: boolean\n /** Don't mask tool-result parts whose original content is shorter\n * than this (estimated tokens). Skips tiny \"ok\"-style success\n * messages where the stub would be larger than the original.\n * Doesn't apply to attachments (always worth masking). */\n minTokens?: number\n /** How many turns to keep in the tail of the conversation, regardless\n * of score. Defaults to 20. */\n keepTurns?: number\n /** How far above the target ratio to trigger a new masking pass.\n * Defaults to 0.25 (25%). */\n delta?: number\n /** Target ratio of used/limit tokens to reach by masking. Defaults to 0.5 (50%). */\n target?: number\n}\n\nexport type MaskOpts = {\n force?: boolean\n limit?: number\n ratio?: number\n tools?: Tool[]\n prompt?: string[]\n}\n\nconst defaults = {\n delta: 0.25,\n enabled: true,\n keepTurns: 40,\n minTokens: 50,\n target: 0.5,\n} as const satisfies Required<MaskerOptions>\n\n/** In-place mask projection for the request stream.\n *\n * The masker replaces low-value parts with stable stubs to keep the\n * projected request within budget. A scoring pass ranks maskable parts\n * by recency, repeated-use shadowing, and per-policy weights; the budget\n * pass masks from lowest score upward until the target is reached or no\n * useful candidates remain.\n *\n * Masking intentionally happens rarely because it changes historical\n * bytes and busts prefix cache. The first pass after startup/session\n * reset always rebuilds the projection. Later passes use a hysteresis\n * threshold: mask toward `target`, then trigger again at roughly\n * `actual + delta`. If the target cannot be reached, the next threshold\n * rises automatically to avoid repeated no-op cache busts.\n */\nexport class Masker {\n #o: MaybeGetter<MaskerOptions>\n #stats = new Map<Role, Record<string, number>>()\n #masked = new Map<string, Map<number, AnyPart>>()\n #threshold?: number\n #agent: Agent\n\n constructor(agent: Agent, opts: MaybeGetter<MaskerOptions> = {}) {\n this.#o = opts\n this.#agent = agent\n agent.on(\"context\", async (ctx) => (ctx.messages = await this.mask(ctx.messages)))\n agent.ctx.on(\"session\", () => this.reset())\n }\n\n get #opts(): Required<MaskerOptions> {\n return { ...defaults, ...toValue(this.#o) }\n }\n\n get enabled(): boolean {\n return this.#opts.enabled\n }\n\n get #session(): Session {\n return this.#agent.session\n }\n\n #maskOpts(opts?: MaskOpts): Required<MaskOpts> {\n const pressure = this.#agent.pressure\n return {\n force: false,\n limit: pressure.limit,\n prompt: this.#agent.prompt,\n ratio: pressure.ratio,\n tools: this.#agent.tools,\n ...opts,\n }\n }\n\n reset(): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = undefined\n }\n\n get stats(): Map<Role, Record<string, number>> {\n return this.#stats\n }\n\n /** Number of messages with at least one masked part. */\n get masked(): number {\n return this.#masked.size\n }\n\n isMasked(msgId: string, partIdx?: number): boolean {\n if (!this.enabled) return false\n const parts = this.#masked.get(msgId)\n if (parts === undefined) return false\n return partIdx === undefined ? parts.size > 0 : parts.has(partIdx)\n }\n\n /** Apply masks and return the projected message array.\n *\n * Always rebuild once after startup/session reset: `pressure.ratio`\n * may describe a previously masked projection, while this instance has\n * no mask decisions yet. After that, rebuild only when projected\n * pressure crosses the hysteresis threshold. */\n async mask(messages: readonly Message[], opts?: MaskOpts): Promise<Message[]> {\n if (!this.enabled || messages.length === 0) return [...messages]\n\n const resolved = this.#maskOpts(opts)\n\n // Masker has not yet been initialized for this session.\n // Use the session's mask checkpoint to restore the last threshold\n // and rebuild the mask decisions up to that point.\n if (this.#threshold === undefined) {\n const cp = this.#session.maskCheckpoint\n if (!cp) {\n // No checkpoint means the session has never been masked before, so\n // just set to the default threshold for the first pass, so that the next pass will\n // trigger only if the pressure exceeds the target + delta.\n this.#threshold = this.#opts.target + this.#opts.delta\n } else {\n this.#threshold = cp.threshold\n const idx = messages.findIndex((m) => m.id === cp.messageId)\n // NOTE: the checkpoint message should exists. If not mask all, just in case\n // Rebuild the mask decisions\n this.#update(idx === -1 ? messages : messages.slice(0, idx + 1), resolved)\n }\n }\n\n // If the current pressure ratio exceeds the threshold, rebuild the mask decisions.\n if (resolved.ratio >= this.#threshold || resolved.force) {\n const messageId = messages.at(-1)?.id\n if (!messageId) throw new Error(\"Message in masker without ID. Should never happen.\")\n // Create a checkpoint first, with the CURRENT threshold, not the new one.\n await this.#session.addMaskCheckpoint({\n messageId,\n threshold: this.#threshold,\n })\n this.#update(messages, resolved)\n }\n return this.#mask(messages)\n }\n\n #stat(role: Role, key: string, n = 1): void {\n const r = this.#stats.get(role) ?? {}\n r[key] = (r[key] ?? 0) + n\n this.#stats.set(role, r)\n }\n\n /** Recompute mask decisions for the current message history. */\n #update(messages: readonly Message[], opts: Required<MaskOpts>): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = this.#opts.target + this.#opts.delta\n const usage = tokenStats(messages, { prompt: opts.prompt, tools: opts.tools })\n // `pressure.ratio` is based on the current projected/masked request,\n // not the raw session. Raw history may exceed model context; masking\n // only needs to keep the projected request in a safe band.\n const targetRatio = Math.max(this.#opts.target, this.#threshold - this.#opts.delta)\n const target = opts.limit * targetRatio\n\n // Return when already under target.\n if (usage.tokens <= target) return\n\n let mask = usage.tokens - target\n\n if (mask === 0) return\n\n const scorer = new ContextScoring()\n const scores = scorer.score(messages)\n const parts: (MsgPart & { mask: () => AnyPart })[] = []\n for (const s of scores) {\n for (const p of s.parts) {\n parts.push({\n ...p,\n mask: () => s.policy.mask(p, s),\n })\n }\n }\n parts.sort((a, b) => a.score - b.score)\n\n let masked = 0\n for (const p of parts) {\n if (mask <= 0) break\n const id = p.message.id\n if (!id) continue\n\n if (p.turn <= this.#opts.keepTurns) continue\n\n const tokens = estimatePart(p.part).tokens\n if (tokens < this.#opts.minTokens) continue\n\n const part = p.mask()\n if (part === p.part) continue\n\n const msgParts = this.#masked.get(id) ?? new Map()\n msgParts.set(p.$p, part)\n this.#stat(\n p.message.role,\n `${p.part.type}${p.part.type === \"tool-result\" || p.part.type === \"tool-call\" ? `:${p.part.name}` : \"\"}`\n )\n this.#masked.set(id, msgParts)\n const delta = tokens - estimatePart(part).tokens\n masked += delta\n mask -= delta\n }\n\n this.#threshold = Math.max(\n (usage.tokens - masked) / opts.limit + this.#opts.delta,\n this.#opts.target + this.#opts.delta\n )\n }\n\n /** Render the current projection using the last computed mask decisions. */\n #mask(messages: readonly Message[]): Message[] {\n return messages.map((m) => {\n if (m.id === undefined || typeof m.content === \"string\") return m\n const masked = m.id ? this.#masked.get(m.id) : undefined\n return masked && masked.size > 0\n ? ({\n ...m,\n content: m.content.map((p, i) => masked.get(i) ?? p),\n } as Message)\n : m\n })\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"notify-DpG_m9iP.mjs","names":["#opts","#ac","#pressureLevel","#lastTime","#lastStep"],"sources":["../src/notify.ts"],"sourcesContent":["import type { Agent } from \"./agent.ts\"\n\nimport { formatDuration } from \"@zaly/shared\"\n\nexport type NotifyContext = {\n agent: Agent\n}\n\nexport type NotifyOptions = {\n idle?: number // seconds of idle time (1800 = 30m)\n periodic?: number // seconds of periodic time notifications (3600 = 1h)\n}\n\nexport class Notifier {\n #opts: Required<NotifyOptions>\n #lastTime?: number\n #lastStep?: number\n /** Highest pressure threshold (from `#pressureLevels`) we've already\n * notified for in this session. Reset to 0 when pressure drops back\n * below the lowest threshold (e.g. after compaction), so a later\n * refill can fire the same level again. */\n #pressureLevel = 0\n #ac?: AbortController\n\n constructor(opts: NotifyOptions = {}) {\n this.#opts = { idle: 30 * 60, periodic: 60 * 60, ...opts }\n }\n\n attach(agent: Agent) {\n agent.on(\"step-start\", () => this.check(agent))\n agent.ctx.on(\"session\", () => this.attachSession(agent))\n this.attachSession(agent)\n }\n\n attachSession(agent: Agent) {\n this.#ac?.abort()\n this.#ac = new AbortController()\n const opts = { signal: this.#ac.signal }\n agent.session\n .on(\n \"compact\",\n ({ node }) => {\n this.#pressureLevel = 0\n agent.notify(\"compacted\", {\n ...this.time(),\n messages_preserved: node.tail,\n trigger: node.trigger,\n })\n },\n opts\n )\n .on(\n \"session-resume\",\n () => {\n agent.notify(\"session-resume\", this.time())\n },\n opts\n )\n .on(\n \"session-start\",\n () => {\n agent.notify(\"session-start\", this.time())\n },\n opts\n )\n .on(\n \"cwd\",\n ({ cwd }) => {\n agent.notify(\"cwd-changed\", { cwd })\n },\n opts\n )\n .on(\n \"model\",\n ({ model, prev }) => {\n agent.notify(\"model-changed\", { current: model, prev })\n },\n opts\n )\n }\n\n time(now = Date.now()) {\n this.#lastTime = now\n return timeInfo(now)\n }\n\n check(agent: Agent) {\n const now = Date.now()\n const lastInfo = this.#lastTime ? timeInfo(this.#lastTime) : undefined\n\n this.#lastStep ??= now\n this.#lastTime ??= now\n\n if (lastInfo && timeInfo(now).date !== lastInfo.date) {\n agent.notify(\"new-day\", this.time(now))\n } else if (now - this.#lastStep > this.#opts.idle * 1000) {\n agent.notify(\"user-returned\", {\n idle: formatDuration(this.#lastStep, { to: now }),\n ...this.time(now),\n })\n } else if (now - this.#lastTime > this.#opts.periodic * 1000) {\n agent.notify(\"time\", this.time(now))\n }\n this.#lastStep = now\n\n // Context-window pressure — denominator is `limit.context` (full\n // window), NOT `maxTokens` (per-request output cap). Notification\n // fires once per discrete level crossing (75% / 85% / 95%) so the\n // model gets at most a few escalating signals per session, not one\n // per step. Resets if pressure drops below the lowest level (e.g.\n // after compaction) so the cycle can fire again later.\n const pressure = agent.pressure\n if (pressure.level > this.#pressureLevel) {\n agent.notify(\"context-pressure\", {\n limit: pressure.limit,\n pct: Math.round(pressure.ratio * 100),\n used: pressure.used,\n })\n this.#pressureLevel = pressure.level\n } else if (pressure.level === 0) this.#pressureLevel = 0\n }\n}\n\nfunction timeInfo(t = Date.now()): {\n day: string\n date: string\n time: string\n tz: string\n} {\n const now = new Date(t)\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone\n return {\n date: now.toLocaleDateString(\"sv-SE\", { timeZone: tz }),\n day: now.toLocaleDateString(\"en-US\", { timeZone: tz, weekday: \"long\" }),\n time: now.toLocaleTimeString(\"sv-SE\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZone: tz,\n }),\n tz,\n }\n}\n"],"mappings":";;AAaA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA;;;;;CAKA,iBAAiB;CACjB;CAEA,YAAY,OAAsB,CAAC,GAAG;EACpC,KAAKA,QAAQ;GAAE,MAAM;GAAS,UAAU;GAAS,GAAG;EAAK;CAC3D;CAEA,OAAO,OAAc;EACnB,MAAM,GAAG,oBAAoB,KAAK,MAAM,KAAK,CAAC;EAC9C,MAAM,IAAI,GAAG,iBAAiB,KAAK,cAAc,KAAK,CAAC;EACvD,KAAK,cAAc,KAAK;CAC1B;CAEA,cAAc,OAAc;EAC1B,KAAKC,KAAK,MAAM;EAChB,KAAKA,MAAM,IAAI,gBAAgB;EAC/B,MAAM,OAAO,EAAE,QAAQ,KAAKA,IAAI,OAAO;EACvC,MAAM,QACH,GACC,YACC,EAAE,WAAW;GACZ,KAAKC,iBAAiB;GACtB,MAAM,OAAO,aAAa;IACxB,GAAG,KAAK,KAAK;IACb,oBAAoB,KAAK;IACzB,SAAS,KAAK;GAChB,CAAC;EACH,GACA,IACF,CAAC,CACA,GACC,wBACM;GACJ,MAAM,OAAO,kBAAkB,KAAK,KAAK,CAAC;EAC5C,GACA,IACF,CAAC,CACA,GACC,uBACM;GACJ,MAAM,OAAO,iBAAiB,KAAK,KAAK,CAAC;EAC3C,GACA,IACF,CAAC,CACA,GACC,QACC,EAAE,UAAU;GACX,MAAM,OAAO,eAAe,EAAE,IAAI,CAAC;EACrC,GACA,IACF,CAAC,CACA,GACC,UACC,EAAE,OAAO,WAAW;GACnB,MAAM,OAAO,iBAAiB;IAAE,SAAS;IAAO;GAAK,CAAC;EACxD,GACA,IACF;CACJ;CAEA,KAAK,MAAM,KAAK,IAAI,GAAG;EACrB,KAAKC,YAAY;EACjB,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,OAAc;EAClB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAKA,YAAY,SAAS,KAAKA,SAAS,IAAI,KAAA;EAE7D,KAAKC,cAAc;EACnB,KAAKD,cAAc;EAEnB,IAAI,YAAY,SAAS,GAAG,CAAC,CAAC,SAAS,SAAS,MAC9C,MAAM,OAAO,WAAW,KAAK,KAAK,GAAG,CAAC;OACjC,IAAI,MAAM,KAAKC,YAAY,KAAKJ,MAAM,OAAO,KAClD,MAAM,OAAO,iBAAiB;GAC5B,MAAM,eAAe,KAAKI,WAAW,EAAE,IAAI,IAAI,CAAC;GAChD,GAAG,KAAK,KAAK,GAAG;EAClB,CAAC;OACI,IAAI,MAAM,KAAKD,YAAY,KAAKH,MAAM,WAAW,KACtD,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,CAAC;EAErC,KAAKI,YAAY;EAQjB,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,QAAQ,KAAKF,gBAAgB;GACxC,MAAM,OAAO,oBAAoB;IAC/B,OAAO,SAAS;IAChB,KAAK,KAAK,MAAM,SAAS,QAAQ,GAAG;IACpC,MAAM,SAAS;GACjB,CAAC;GACD,KAAKA,iBAAiB,SAAS;EACjC,OAAO,IAAI,SAAS,UAAU,GAAG,KAAKA,iBAAiB;CACzD;AACF;AAEA,SAAS,SAAS,IAAI,KAAK,IAAI,GAK7B;CACA,MAAM,MAAM,IAAI,KAAK,CAAC;CACtB,MAAM,KAAK,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC;CACnD,OAAO;EACL,MAAM,IAAI,mBAAmB,SAAS,EAAE,UAAU,GAAG,CAAC;EACtD,KAAK,IAAI,mBAAmB,SAAS;GAAE,UAAU;GAAI,SAAS;EAAO,CAAC;EACtE,MAAM,IAAI,mBAAmB,SAAS;GACpC,MAAM;GACN,QAAQ;GACR,UAAU;EACZ,CAAC;EACD;CACF;AACF"}
1
+ {"version":3,"file":"notify-DpG_m9iP.mjs","names":[],"sources":["../src/notify.ts"],"sourcesContent":["import type { Agent } from \"./agent.ts\"\n\nimport { formatDuration } from \"@zaly/shared\"\n\nexport type NotifyContext = {\n agent: Agent\n}\n\nexport type NotifyOptions = {\n idle?: number // seconds of idle time (1800 = 30m)\n periodic?: number // seconds of periodic time notifications (3600 = 1h)\n}\n\nexport class Notifier {\n #opts: Required<NotifyOptions>\n #lastTime?: number\n #lastStep?: number\n /** Highest pressure threshold (from `#pressureLevels`) we've already\n * notified for in this session. Reset to 0 when pressure drops back\n * below the lowest threshold (e.g. after compaction), so a later\n * refill can fire the same level again. */\n #pressureLevel = 0\n #ac?: AbortController\n\n constructor(opts: NotifyOptions = {}) {\n this.#opts = { idle: 30 * 60, periodic: 60 * 60, ...opts }\n }\n\n attach(agent: Agent) {\n agent.on(\"step-start\", () => this.check(agent))\n agent.ctx.on(\"session\", () => this.attachSession(agent))\n this.attachSession(agent)\n }\n\n attachSession(agent: Agent) {\n this.#ac?.abort()\n this.#ac = new AbortController()\n const opts = { signal: this.#ac.signal }\n agent.session\n .on(\n \"compact\",\n ({ node }) => {\n this.#pressureLevel = 0\n agent.notify(\"compacted\", {\n ...this.time(),\n messages_preserved: node.tail,\n trigger: node.trigger,\n })\n },\n opts\n )\n .on(\n \"session-resume\",\n () => {\n agent.notify(\"session-resume\", this.time())\n },\n opts\n )\n .on(\n \"session-start\",\n () => {\n agent.notify(\"session-start\", this.time())\n },\n opts\n )\n .on(\n \"cwd\",\n ({ cwd }) => {\n agent.notify(\"cwd-changed\", { cwd })\n },\n opts\n )\n .on(\n \"model\",\n ({ model, prev }) => {\n agent.notify(\"model-changed\", { current: model, prev })\n },\n opts\n )\n }\n\n time(now = Date.now()) {\n this.#lastTime = now\n return timeInfo(now)\n }\n\n check(agent: Agent) {\n const now = Date.now()\n const lastInfo = this.#lastTime ? timeInfo(this.#lastTime) : undefined\n\n this.#lastStep ??= now\n this.#lastTime ??= now\n\n if (lastInfo && timeInfo(now).date !== lastInfo.date) {\n agent.notify(\"new-day\", this.time(now))\n } else if (now - this.#lastStep > this.#opts.idle * 1000) {\n agent.notify(\"user-returned\", {\n idle: formatDuration(this.#lastStep, { to: now }),\n ...this.time(now),\n })\n } else if (now - this.#lastTime > this.#opts.periodic * 1000) {\n agent.notify(\"time\", this.time(now))\n }\n this.#lastStep = now\n\n // Context-window pressure — denominator is `limit.context` (full\n // window), NOT `maxTokens` (per-request output cap). Notification\n // fires once per discrete level crossing (75% / 85% / 95%) so the\n // model gets at most a few escalating signals per session, not one\n // per step. Resets if pressure drops below the lowest level (e.g.\n // after compaction) so the cycle can fire again later.\n const pressure = agent.pressure\n if (pressure.level > this.#pressureLevel) {\n agent.notify(\"context-pressure\", {\n limit: pressure.limit,\n pct: Math.round(pressure.ratio * 100),\n used: pressure.used,\n })\n this.#pressureLevel = pressure.level\n } else if (pressure.level === 0) this.#pressureLevel = 0\n }\n}\n\nfunction timeInfo(t = Date.now()): {\n day: string\n date: string\n time: string\n tz: string\n} {\n const now = new Date(t)\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone\n return {\n date: now.toLocaleDateString(\"sv-SE\", { timeZone: tz }),\n day: now.toLocaleDateString(\"en-US\", { timeZone: tz, weekday: \"long\" }),\n time: now.toLocaleTimeString(\"sv-SE\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZone: tz,\n }),\n tz,\n }\n}\n"],"mappings":";;AAaA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA;;;;;CAKA,iBAAiB;CACjB;CAEA,YAAY,OAAsB,CAAC,GAAG;EACpC,KAAK,QAAQ;GAAE,MAAM;GAAS,UAAU;GAAS,GAAG;EAAK;CAC3D;CAEA,OAAO,OAAc;EACnB,MAAM,GAAG,oBAAoB,KAAK,MAAM,KAAK,CAAC;EAC9C,MAAM,IAAI,GAAG,iBAAiB,KAAK,cAAc,KAAK,CAAC;EACvD,KAAK,cAAc,KAAK;CAC1B;CAEA,cAAc,OAAc;EAC1B,KAAK,KAAK,MAAM;EAChB,KAAK,MAAM,IAAI,gBAAgB;EAC/B,MAAM,OAAO,EAAE,QAAQ,KAAK,IAAI,OAAO;EACvC,MAAM,QACH,GACC,YACC,EAAE,WAAW;GACZ,KAAK,iBAAiB;GACtB,MAAM,OAAO,aAAa;IACxB,GAAG,KAAK,KAAK;IACb,oBAAoB,KAAK;IACzB,SAAS,KAAK;GAChB,CAAC;EACH,GACA,IACF,CAAC,CACA,GACC,wBACM;GACJ,MAAM,OAAO,kBAAkB,KAAK,KAAK,CAAC;EAC5C,GACA,IACF,CAAC,CACA,GACC,uBACM;GACJ,MAAM,OAAO,iBAAiB,KAAK,KAAK,CAAC;EAC3C,GACA,IACF,CAAC,CACA,GACC,QACC,EAAE,UAAU;GACX,MAAM,OAAO,eAAe,EAAE,IAAI,CAAC;EACrC,GACA,IACF,CAAC,CACA,GACC,UACC,EAAE,OAAO,WAAW;GACnB,MAAM,OAAO,iBAAiB;IAAE,SAAS;IAAO;GAAK,CAAC;EACxD,GACA,IACF;CACJ;CAEA,KAAK,MAAM,KAAK,IAAI,GAAG;EACrB,KAAK,YAAY;EACjB,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,OAAc;EAClB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAK,YAAY,SAAS,KAAK,SAAS,IAAI,KAAA;EAE7D,KAAK,cAAc;EACnB,KAAK,cAAc;EAEnB,IAAI,YAAY,SAAS,GAAG,CAAC,CAAC,SAAS,SAAS,MAC9C,MAAM,OAAO,WAAW,KAAK,KAAK,GAAG,CAAC;OACjC,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,OAAO,KAClD,MAAM,OAAO,iBAAiB;GAC5B,MAAM,eAAe,KAAK,WAAW,EAAE,IAAI,IAAI,CAAC;GAChD,GAAG,KAAK,KAAK,GAAG;EAClB,CAAC;OACI,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,WAAW,KACtD,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,CAAC;EAErC,KAAK,YAAY;EAQjB,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,QAAQ,KAAK,gBAAgB;GACxC,MAAM,OAAO,oBAAoB;IAC/B,OAAO,SAAS;IAChB,KAAK,KAAK,MAAM,SAAS,QAAQ,GAAG;IACpC,MAAM,SAAS;GACjB,CAAC;GACD,KAAK,iBAAiB,SAAS;EACjC,OAAO,IAAI,SAAS,UAAU,GAAG,KAAK,iBAAiB;CACzD;AACF;AAEA,SAAS,SAAS,IAAI,KAAK,IAAI,GAK7B;CACA,MAAM,MAAM,IAAI,KAAK,CAAC;CACtB,MAAM,KAAK,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC;CACnD,OAAO;EACL,MAAM,IAAI,mBAAmB,SAAS,EAAE,UAAU,GAAG,CAAC;EACtD,KAAK,IAAI,mBAAmB,SAAS;GAAE,UAAU;GAAI,SAAS;EAAO,CAAC;EACtE,MAAM,IAAI,mBAAmB,SAAS;GACpC,MAAM;GACN,QAAQ;GACR,UAAU;EACZ,CAAC;EACD;CACF;AACF"}