@agentconnect.md/daemon 1.0.0-rc.245 → 1.0.0-rc.247

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.
@@ -1,6 +1,7 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "./rolldown-runtime-CE-6LUnI.js";
2
2
  import { t as require_src$1 } from "./src-DDtlVutj.js";
3
- import { T as NEVER, _ as record, a as array, b as unknown, c as discriminatedUnion, d as literal, l as int, m as object, n as _enum, o as boolean, p as number, u as intersection, v as string, x as url, y as union } from "./schemas-CQkO9hC5.js";
3
+ import { T as NEVER, _ as record, a as array, b as unknown, d as literal, l as int, m as object, o as boolean, p as number, u as intersection, v as string, x as url, y as union } from "./schemas-CQkO9hC5.js";
4
+ import { b as MEMORY_INDEX, d as normalizeGitUrl } from "./load-agents-NvLpcg5H.js";
4
5
  import { c as registryPath, s as registryCachePath } from "./paths-DEcUO8lC.js";
5
6
  import { delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
6
7
  import { accessSync, constants, existsSync, mkdirSync, promises, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -8,7 +9,6 @@ import { spawn } from "child_process";
8
9
  import { EventEmitter } from "node:events";
9
10
  import { execFile, spawn as spawn$1 } from "node:child_process";
10
11
  import { homedir } from "node:os";
11
- import { randomUUID } from "node:crypto";
12
12
  import { Readable, Writable } from "node:stream";
13
13
  //#region src/runtimes/registry.ts
14
14
  const PackageDistSchema = object({
@@ -371,1561 +371,6 @@ function toolsForIntegrations(integrations) {
371
371
  return tools;
372
372
  }
373
373
  //#endregion
374
- //#region ../protocol/dist/envelope.js
375
- /**
376
- * The protocol envelope — every frame, both directions, is wrapped in this.
377
- * Mirrors daemon-cp-ws-protocol.md §1.1.
378
- *
379
- * `payload` is left as `unknown` here and validated by the per-type schema in
380
- * `frames/*` via `FRAME_SCHEMAS[type]` (see `frame.ts`). This two-step parse is
381
- * what lets the codec answer an unknown `type` with `UNKNOWN_FRAME` (a REP)
382
- * instead of a hard close — forward-compat, protocol §1.
383
- */
384
- const Envelope = object({
385
- v: literal(1),
386
- id: string().uuid(),
387
- ts: string().datetime(),
388
- type: string(),
389
- corr: string().uuid().optional(),
390
- payload: unknown()
391
- });
392
- object({
393
- epoch: number().int(),
394
- agentId: string().uuid().optional(),
395
- launchId: string().uuid().optional()
396
- });
397
- /** The all-zero UUID used when a frame is malformed past the point of reading `id`. */
398
- const NIL_UUID = "00000000-0000-0000-0000-000000000000";
399
- /**
400
- * Builds the envelope schema for one frame `type` — the `type` literal plus the
401
- * typed payload — so a wire's discriminated union infers `payload` precisely.
402
- * Shared by the daemon↔CP union (`frame.ts`) and the relay wire unions
403
- * (`frames/relay-cp.ts` / `frames/relay-daemon.ts`), which are deliberately
404
- * SEPARATE unions over the same envelope shape (shared-bot-relay.md §8).
405
- */
406
- function frameSchema(type, payload) {
407
- return object({
408
- v: literal(1),
409
- id: string().uuid(),
410
- ts: string().datetime(),
411
- type: literal(type),
412
- corr: string().uuid().optional(),
413
- payload
414
- });
415
- }
416
- //#endregion
417
- //#region ../protocol/dist/frames/auth.js
418
- /**
419
- * Auth & identity — protocol §3.1 / §3.2.
420
- *
421
- * `auth` is the first frame after the socket opens. `auth/ok` carries the
422
- * minted `sessionEpoch` (the global fencing token), heartbeat cadence, and the
423
- * resume verdict.
424
- */
425
- const AuthReq = object({
426
- apiKey: string(),
427
- daemonId: string().uuid().optional(),
428
- machineId: string().uuid().optional(),
429
- attestation: string().optional(),
430
- agentVersion: string(),
431
- resume: object({ lastEpoch: number().int() }).optional()
432
- });
433
- const AuthOk = object({
434
- daemonId: string().uuid(),
435
- sessionEpoch: number().int(),
436
- heartbeatSec: number().int(),
437
- serverTime: string().datetime(),
438
- webAppUrl: string().optional(),
439
- orgSlug: string().optional(),
440
- resume: object({ accepted: boolean() }).optional()
441
- });
442
- //#endregion
443
- //#region ../protocol/dist/frames/route.js
444
- /**
445
- * Routing & orchestration (C→D control) — protocol §5.
446
- *
447
- * `SessionKey` is the canonical session primitive shared across route/*,
448
- * agent/*, and event/session. Its canonical string form is
449
- * `${platform}:${channel}:${thread ?? "-"}`.
450
- */
451
- const Platform = _enum([
452
- "slack",
453
- "telegram",
454
- "webchat",
455
- "discord"
456
- ]);
457
- const SessionKey = object({
458
- platform: Platform,
459
- channel: string(),
460
- thread: string().optional()
461
- });
462
- /** Trigger-matching rule for a binding (protocol §5.1). */
463
- const BindRule = object({ match: discriminatedUnion("kind", [
464
- object({ kind: literal("mention") }),
465
- object({ kind: literal("dm") }),
466
- object({
467
- kind: literal("keyword"),
468
- value: string()
469
- }),
470
- object({ kind: literal("auto") })
471
- ]) });
472
- const RouteAssign = object({
473
- sessionKey: SessionKey,
474
- agentId: string().uuid(),
475
- workspaceId: string().uuid(),
476
- bindRules: array(BindRule).default([])
477
- });
478
- const RouteAssignAck = object({
479
- ok: boolean(),
480
- sessionKey: SessionKey,
481
- reason: string().optional()
482
- });
483
- const RouteUpdate = object({
484
- routingEpoch: number().int(),
485
- rules: array(object({
486
- match: unknown(),
487
- agentId: string().uuid()
488
- }))
489
- });
490
- /** Graceful scale-down / rebalance — protocol §5.3. */
491
- const Drain = object({
492
- scope: union([
493
- object({
494
- kind: literal("agent"),
495
- agentId: string().uuid()
496
- }),
497
- object({ kind: literal("daemon") }),
498
- object({
499
- kind: literal("session"),
500
- sessionKey: SessionKey
501
- })
502
- ]),
503
- deadline: string().datetime()
504
- });
505
- const DrainProgress = object({
506
- remaining: number().int(),
507
- drained: array(SessionKey)
508
- });
509
- const DrainDone = object({ released: array(SessionKey) });
510
- //#endregion
511
- //#region ../protocol/dist/frames/cron.js
512
- /**
513
- * Cron sinks to the daemon (D5) — protocol §5.4.
514
- *
515
- * A cron periodically triggers ONE AGENT with a synthetic prompt (`trigger`) to
516
- * carry out some work. The CP owns the definition; the daemon owns firing +
517
- * last-run persistence, so crons fire even when the CP is down. On receipt the
518
- * daemon persists the def into the owning agent's `agent.json` `crons[]` (the
519
- * single source of truth, same model as integrations) — surviving a restart
520
- * with the CP down.
521
- *
522
- * `target` is OPTIONAL output routing: when present, the daemon posts the
523
- * trigger as a real message in that channel and the agent's session replies in
524
- * its thread; when absent the fire is headless (the agent works with no
525
- * platform output).
526
- */
527
- const CronTarget = object({
528
- platform: _enum([
529
- "slack",
530
- "telegram",
531
- "discord"
532
- ]).default("slack"),
533
- channel: string(),
534
- integrationId: string().uuid().optional()
535
- });
536
- const CronUpsert = object({
537
- cronId: string().uuid(),
538
- agentId: string().uuid(),
539
- schedule: string(),
540
- target: CronTarget.optional(),
541
- trigger: string(),
542
- enabled: boolean().default(true)
543
- });
544
- const CronRemove = object({ cronId: string().uuid() });
545
- /**
546
- * `cron/report` (D→C EVT, fire-and-forget) — one CP-owned cron fired. The
547
- * daemon stamps the fire into its local store first (it stays authoritative,
548
- * §5.4) and reports it here so the console's `lastRunAt` converges; the CP
549
- * upsert is latest-wins, so the daemon re-asserting its stored stamps on
550
- * reconnect (fires while the CP was unreachable) is idempotent and can never
551
- * regress the value. Hand-authored (no-origin) crons are never reported.
552
- *
553
- * Two reports per fire, keyed by `(cronId, firedAt)`: the FIRE report (no
554
- * `status`) opens the run, and the COMPLETION report (with `status` + outcome
555
- * fields) closes it once the dispatched turn ends — feeding the console's
556
- * per-schedule run history. A completion without a prior fire report (CP was
557
- * down at fire time) still creates the run row.
558
- */
559
- const CronRunStatus = _enum(["success", "failed"]);
560
- const CronReport = object({
561
- cronId: string().uuid(),
562
- agentId: string().uuid(),
563
- firedAt: string().datetime(),
564
- status: CronRunStatus.optional(),
565
- durationMs: number().int().nonnegative().optional(),
566
- sessionId: string().optional(),
567
- reason: string().optional()
568
- });
569
- /**
570
- * `cron/run` (C→D REQ → ack) — fire one CP-owned cron NOW (console "Run now").
571
- * The daemon accepts (`ok:true`) and runs the fire asynchronously — outcome
572
- * arrives as normal `cron/report`s; `ok:false` when it holds no such cron.
573
- */
574
- const CronRunNow = object({ cronId: string().uuid() });
575
- //#endregion
576
- //#region ../protocol/dist/frames/secrets.js
577
- /**
578
- * Secrets (C5 ↔ D10) — protocol §6.
579
- *
580
- * Lease-based, no plaintext on the wire or in PG. Every frame carries a
581
- * REFERENCE to a Vault/KMS path, never the secret material itself.
582
- */
583
- const SecretsRequest = object({ scope: object({
584
- platform: Platform,
585
- workspaceId: string().uuid()
586
- }) });
587
- const SecretsGrant = object({
588
- leaseId: string().uuid(),
589
- scope: object({
590
- platform: string(),
591
- workspaceId: string().uuid()
592
- }),
593
- ref: string(),
594
- ttl: number().int(),
595
- renewBeforeSec: number().int()
596
- });
597
- const SecretsRenew = object({ leaseId: string().uuid() });
598
- const SecretsRevoke = object({
599
- leaseId: string().uuid(),
600
- reason: string()
601
- });
602
- /** 🅼 Direct-to-store upload/download grant — protocol §3.2 / frame #25. */
603
- const ScopeAttestation = object({
604
- machineId: string().uuid(),
605
- scope: _enum([
606
- "attachment.put",
607
- "attachment.get",
608
- "facts.put"
609
- ]),
610
- resourceRef: string(),
611
- jws: string(),
612
- exp: string().datetime()
613
- });
614
- //#endregion
615
- //#region ../protocol/dist/frames/agent.js
616
- /**
617
- * Agent lifecycle (protocol §4.4, §7.4, §8).
618
- *
619
- * There is no CP→daemon prompt-delivery frame: the daemon prompts an agent from
620
- * its own ingress (platform adapters, and webchat over its own `webchat/*`
621
- * frames), never the CP. The old `agent/prompt` + per-agent `seq` machinery was
622
- * reserved infrastructure with no live caller and has been removed.
623
- */
624
- /**
625
- * Where the agent runs. Two modes; the **path is always daemon-generated** —
626
- * never specified by the caller (UX picks the mode, the machine owns the dir).
627
- *
628
- * - `scratch`: a fresh empty working dir on the machine, no repo.
629
- * - `github`: the daemon clones `gitRepo` @ `branch` and runs the agent in
630
- * `agentDir` (a subdir of the repo, repo-root if omitted). **Multiple agents
631
- * may share one repo** — they differ by `agentDir`, so the repo is not an
632
- * owned entity, just shared config on each agent.
633
- */
634
- const AgentWorkspace = discriminatedUnion("mode", [object({ mode: literal("scratch") }), object({
635
- mode: literal("github"),
636
- gitRepo: string(),
637
- branch: string().default("main"),
638
- agentDir: string().optional(),
639
- gitCredential: _enum(["github-app"]).optional()
640
- })]);
641
- /**
642
- * MCP-server name reserved for the daemon's own injected stdio bridge (its
643
- * platform tools). A config-defined or agent-enabled server under this name
644
- * would collide with the bridge entry at ACP `session/new`, so the daemon
645
- * strips it and the CP rejects it in `AgentSpec.mcpServers` at the API edge.
646
- */
647
- const RESERVED_MCP_SERVER_NAME = "agentconnect";
648
- /**
649
- * The editable agent definition the CP owns and the daemon needs to run it:
650
- * prompt + runtime selection. Raft ships this in the launch config and the
651
- * daemon synthesizes the system prompt locally; `description` IS the prompt.
652
- */
653
- const AgentSpec = object({
654
- name: string(),
655
- description: string().optional(),
656
- runtime: string().optional(),
657
- model: string().nullable().optional(),
658
- reasoningEffort: string().nullable().optional(),
659
- executionMode: string().optional(),
660
- outputMode: _enum([
661
- "low",
662
- "medium",
663
- "high"
664
- ]).optional(),
665
- fastMode: boolean().optional(),
666
- permissionMode: string().nullable().optional(),
667
- pause: boolean().optional(),
668
- workspace: AgentWorkspace.optional(),
669
- env: record(string(), string()).optional(),
670
- memory: object({ provider: _enum([
671
- "native",
672
- "managed",
673
- "external"
674
- ]) }).optional(),
675
- mcpServers: array(string()).default([])
676
- });
677
- const AgentLaunch = object({
678
- agentId: string().uuid(),
679
- runtime: string(),
680
- workspaceId: string().uuid(),
681
- capabilities: array(string()),
682
- spec: AgentSpec,
683
- mode: _enum(["long_lived", "per_turn"]).default("long_lived")
684
- });
685
- /**
686
- * Live agent CRUD (C→D): the console edited an agent's spec; push it so a
687
- * running daemon reloads without waiting for the next launch. `agent/remove`
688
- * tears the agent down. Deleting an agent never relaunches it.
689
- */
690
- const AgentUpsert = object({
691
- agentId: string().uuid(),
692
- spec: AgentSpec
693
- });
694
- const AgentRemove = object({ agentId: string().uuid() });
695
- const AgentLaunched = object({
696
- agentId: string().uuid(),
697
- launchId: string().uuid(),
698
- acpSessionId: string().optional(),
699
- startedAt: string().datetime(),
700
- runtime: string()
701
- });
702
- const AgentStop = object({
703
- agentId: string().uuid(),
704
- launchId: string().uuid(),
705
- reason: string()
706
- });
707
- const AgentActivity = object({
708
- agentId: string().uuid(),
709
- launchId: string().uuid(),
710
- state: _enum([
711
- "thinking",
712
- "tool_call",
713
- "awaiting_permission",
714
- "idle"
715
- ]),
716
- ts: string().datetime()
717
- });
718
- const AgentScopeDenied = object({
719
- agentId: string().uuid(),
720
- launchId: string().uuid(),
721
- capability: string()
722
- });
723
- //#endregion
724
- //#region ../protocol/dist/frames/integration.js
725
- /**
726
- * Platform integration distribution (C→D) — the Slack "install" flow.
727
- *
728
- * The Control Plane is the source of truth for platform integrations and pushes
729
- * them to the daemon that owns the integration's agent (`integration/upsert`, and
730
- * the reconcile snapshot `RegisterOk.integrations[]`). The daemon opens the Socket
731
- * Mode connection from the delivered config (see slack/connection.ts).
732
- *
733
- * SECURITY: `integration/upsert` and `RegisterOk.integrations[]` carry PLAINTEXT
734
- * platform tokens (botToken/appToken). These payloads MUST NEVER be logged — no
735
- * body dump on decode error, no register/ok snapshot debug dump. The daemon
736
- * persists them into the owning agent's local `agent.json` (same trust boundary
737
- * as hand-authored agents, which already keep tokens there) so integrations
738
- * survive a restart with the CP down.
739
- *
740
- * `signingSecret` is intentionally absent: Socket Mode authenticates with the
741
- * app-level token, so the daemon never needs a signing secret.
742
- */
743
- /** Trigger match — mirrors the daemon BindRuleConfig.match (agents/agent-schema.ts). */
744
- const BindMatch = discriminatedUnion("kind", [
745
- object({ kind: literal("mention") }),
746
- object({ kind: literal("dm") }),
747
- object({
748
- kind: literal("keyword"),
749
- value: string()
750
- }),
751
- object({ kind: literal("auto") })
752
- ]);
753
- /** One channel/thread trigger binding — mirrors the daemon BindRuleConfig. */
754
- const IntegrationBindRule = object({
755
- channel: string().optional(),
756
- thread: string().optional(),
757
- match: BindMatch
758
- });
759
- /** The Slack config the daemon needs to open + route a socket (no signingSecret). */
760
- const IntegrationSlackConfig = object({
761
- botToken: string(),
762
- appToken: string(),
763
- allowedUserIds: array(string()).default([]),
764
- bindRules: array(IntegrationBindRule).default([])
765
- });
766
- /**
767
- * The Telegram config the daemon needs to open long-polling + route (grammY).
768
- * Telegram has a SINGLE BotFather HTTP token — no app-level token and no signing
769
- * secret (long-polling authenticates every getUpdates call with the bot token).
770
- */
771
- const IntegrationTelegramConfig = object({
772
- botToken: string(),
773
- allowedUserIds: array(string()).default([]),
774
- bindRules: array(IntegrationBindRule).default([])
775
- });
776
- /**
777
- * The Discord config the daemon needs to open the Gateway + route (discord.js).
778
- * Discord authenticates the Gateway with a SINGLE bot token — no Slack-style
779
- * app-level token and no signing secret. `applicationId` is public metadata (the
780
- * client id for the OAuth2 bot-invite URL); it is not secret material.
781
- */
782
- const IntegrationDiscordConfig = object({
783
- botToken: string(),
784
- applicationId: string().optional(),
785
- allowedUserIds: array(string()).default([]),
786
- bindRules: array(IntegrationBindRule).default([])
787
- });
788
- /**
789
- * One platform integration, owned by exactly one agent. Also the element type of
790
- * `RegisterOk.integrations[]` (the per-daemon reconcile set). Discriminated on
791
- * `platform`: the daemon opens a Slack Socket Mode connection, a Telegram
792
- * long-poll, or a Discord Gateway from whichever variant is delivered.
793
- */
794
- const IntegrationSpec = discriminatedUnion("platform", [
795
- object({
796
- integrationId: string().uuid(),
797
- agentId: string().uuid(),
798
- platform: literal("slack"),
799
- slack: IntegrationSlackConfig
800
- }),
801
- object({
802
- integrationId: string().uuid(),
803
- agentId: string().uuid(),
804
- platform: literal("telegram"),
805
- telegram: IntegrationTelegramConfig
806
- }),
807
- object({
808
- integrationId: string().uuid(),
809
- agentId: string().uuid(),
810
- platform: literal("discord"),
811
- discord: IntegrationDiscordConfig
812
- })
813
- ]);
814
- /** C→D EVT — install/update an integration on the owning agent's daemon. */
815
- const IntegrationUpsert = IntegrationSpec;
816
- /** C→D EVT — remove an integration from the daemon. */
817
- const IntegrationRemove = object({ integrationId: string().uuid() });
818
- /** One channel the bot is currently a member of (metadata only — no messages). */
819
- const IntegrationChannel = object({
820
- id: string(),
821
- name: string().optional(),
822
- isPrivate: boolean().optional()
823
- });
824
- /**
825
- * D→C EVT — the FULL set of channels the integration's bot is a member of
826
- * (fire-and-forget, latest-wins). Emitted on socket start and whenever the bot
827
- * is invited to / removed from a channel, so the console can offer per-channel
828
- * trigger config. Channel names are control metadata, never message content.
829
- */
830
- const IntegrationChannels = object({
831
- integrationId: string().uuid(),
832
- channels: array(IntegrationChannel)
833
- });
834
- //#endregion
835
- //#region ../protocol/dist/frames/register.js
836
- /**
837
- * Capability upload + the reconcile snapshot — protocol §3.3.
838
- *
839
- * `register/ok` is the authoritative source of truth: the daemon converges its
840
- * local cache to it. CP wins all conflicts, so re-issuing the same snapshot is
841
- * idempotent.
842
- */
843
- const RegisterReq = object({
844
- host: string(),
845
- capabilities: object({
846
- platforms: array(Platform),
847
- runtimes: array(string()),
848
- acp: boolean(),
849
- features: array(string()).default([])
850
- }),
851
- maxAgents: number().int(),
852
- localState: object({
853
- assignments: array(string()),
854
- crons: array(string()),
855
- leases: array(string())
856
- })
857
- });
858
- /**
859
- * One relay the daemon SHOULD hold an outbound WS to (shared-bot-relay.md §5).
860
- * The roster is all-to-all by design: a webchat/webhook landing on ANY relay
861
- * instance must find this daemon's connection without cross-instance forwarding.
862
- * That only holds if `url` (the relay's registered `daemonUrl`) routes to that
863
- * SPECIFIC instance — the daemon confirms the landing spot against
864
- * `rd/hello/ok.relayId` and treats a mismatch as a deployment misroute.
865
- */
866
- const RelayRosterEntry = object({
867
- relayId: string().uuid(),
868
- url: string()
869
- });
870
- /**
871
- * C→D EVT (`relay/roster`) — hot roster update (relay registered / swept).
872
- * Carries the WHOLE desired set, same converge-don't-diff semantics as the
873
- * `register/ok.relays` snapshot it refreshes.
874
- */
875
- const RelayRosterUpdate = object({ relays: array(RelayRosterEntry) });
876
- const RegisterOk = object({
877
- routingEpoch: number().int(),
878
- assignments: array(RouteAssign),
879
- agents: array(AgentSpec.extend({ agentId: string().uuid() })).default([]),
880
- crons: array(CronUpsert),
881
- integrations: array(IntegrationSpec).default([]),
882
- leases: array(SecretsGrant),
883
- relays: array(RelayRosterEntry).default([]),
884
- drop: object({
885
- assignments: array(string()),
886
- crons: array(string())
887
- })
888
- });
889
- //#endregion
890
- //#region ../protocol/dist/frames/gitcred.js
891
- /**
892
- * Git credentials (github-app workspaces) — daemon-pulled, CP-minted.
893
- *
894
- * The CP holds the GitHub App private key and mints short-lived (1h,
895
- * non-renewable) installation access tokens scoped to a single repository;
896
- * the daemon pulls one on demand right before a remote git operation and
897
- * holds it in memory only. Unlike `secrets/*` (lease + reference semantics,
898
- * still unwired), the grant here carries the TOKEN MATERIAL itself — same
899
- * plaintext-over-TLS-WS posture as `integration/upsert`, and the same
900
- * discipline: **never log the payload**.
901
- *
902
- * The daemon may only name an agentId — the CP resolves agent → workspace →
903
- * repo → installation itself, so a daemon can never pick a repo it wasn't
904
- * assigned. Failures come back as correlated `error` REPs: `SCOPE_DENIED`
905
- * (not a github-app workspace, or agent not placed on this daemon — stop
906
- * asking), `LEASE_DENIED` (installation uninstalled/suspended or the repo
907
- * left its grant set — recoverable only by an operator), `RATE_LIMITED`,
908
- * `INTERNAL`.
909
- */
910
- const GitCredRequest = object({
911
- agentId: string().uuid(),
912
- reason: _enum([
913
- "clone",
914
- "fetch",
915
- "pull",
916
- "push",
917
- "helper"
918
- ]).optional()
919
- });
920
- const GitCredGrant = object({
921
- username: literal("x-access-token"),
922
- token: string(),
923
- ttlSec: number().int(),
924
- expiresAt: string().datetime(),
925
- repoFullName: string(),
926
- access: _enum(["read", "write"])
927
- });
928
- //#endregion
929
- //#region ../protocol/dist/frames/session.js
930
- /**
931
- * Session read-back (C→D REQ → REP) — the console's on-demand pulls.
932
- *
933
- * Session bodies are daemon-local (body-locality, §1/§12). The CP may persist a
934
- * metadata-only session-list snapshot from daemon `event/session` events, while
935
- * transcript/tool bodies are pulled live from the owning daemon and proxied to
936
- * the console only for display.
937
- *
938
- * - `session/list`: the daemon's local metadata projection, used for snapshots
939
- * and direct read-back/debugging.
940
- * - `session/history`: one cursor-paginated page of a session's transcript.
941
- */
942
- /**
943
- * Per-session token accounting, metered by the daemon from the agent's ACP usage
944
- * stream. Token counts are CUMULATIVE across the session's turns (summed from each
945
- * turn's `PromptResponse.usage`); context/cost are the LATEST snapshot from the
946
- * agent's `usage_update` stream. Every field is optional — a runtime that reports
947
- * no usage yields an absent field, not a zero.
948
- */
949
- const SessionUsage = object({
950
- totalTokens: number().int().optional(),
951
- inputTokens: number().int().optional(),
952
- outputTokens: number().int().optional(),
953
- thoughtTokens: number().int().optional(),
954
- cachedReadTokens: number().int().optional(),
955
- cachedWriteTokens: number().int().optional(),
956
- contextUsed: number().int().optional(),
957
- contextSize: number().int().optional(),
958
- costAmount: number().optional(),
959
- costCurrency: string().optional()
960
- });
961
- /** One row in the session list (metadata + console metrics; NOT the transcript). */
962
- const SessionListItem = object({
963
- sessionId: string(),
964
- sessionKey: SessionKey,
965
- agentId: string().uuid(),
966
- title: string().optional(),
967
- status: string().optional(),
968
- lastActivityAt: string().optional(),
969
- usage: SessionUsage.optional(),
970
- triggeredBy: string().optional(),
971
- channelName: string().optional(),
972
- triggeredByName: string().optional(),
973
- threadUrl: string().optional()
974
- });
975
- /** C→D REQ: list the daemon's sessions (optionally just one agent's). */
976
- const SessionListReq = object({ agentId: string().uuid().optional() });
977
- /** D→C REP (corr = req id): the daemon's current sessions. */
978
- const SessionListPage = object({ sessions: array(SessionListItem) });
979
- object({
980
- toolCallId: string(),
981
- kind: string().optional(),
982
- status: string().optional(),
983
- rawInput: unknown().optional(),
984
- rawOutput: unknown().optional(),
985
- content: array(unknown()).optional(),
986
- locations: array(object({
987
- path: string(),
988
- line: number().int().optional()
989
- })).optional(),
990
- truncated: boolean().optional()
991
- });
992
- /** One message in a session transcript page (a body — returned only for display). */
993
- const SessionMessage = object({
994
- seq: number().int(),
995
- sender: string(),
996
- senderName: string().optional(),
997
- ts: string(),
998
- kind: string(),
999
- text: string(),
1000
- toolCallId: string().optional(),
1001
- toolStatus: string().optional(),
1002
- toolKind: string().optional(),
1003
- body: string().optional(),
1004
- bodyTruncated: boolean().optional(),
1005
- bodyBytes: number().int().optional()
1006
- });
1007
- /** C→D REQ: fetch one page of a session's history from the owning daemon. */
1008
- const SessionHistoryReq = object({
1009
- sessionId: string(),
1010
- cursor: string().optional(),
1011
- limit: number().int().positive().max(200).default(50)
1012
- });
1013
- /** D→C REP (corr = the req id): a page of messages + the cursor for the next page. */
1014
- const SessionHistoryPage = object({
1015
- sessionId: string(),
1016
- messages: array(SessionMessage),
1017
- nextCursor: string().optional()
1018
- });
1019
- /**
1020
- * C→D REQ: fetch the FULL (untruncated) ToolBody JSON for one tool call, chunked.
1021
- * The inline `SessionMessage.body` preview is capped at 32 KiB; when a body is
1022
- * bigger the console pages the whole thing back through this frame by offset.
1023
- */
1024
- const SessionToolBodyReq = object({
1025
- sessionId: string(),
1026
- toolCallId: string(),
1027
- offset: number().int().nonnegative().default(0)
1028
- });
1029
- /** D→C REP (corr = the req id): one frame-budgeted byte slice of the full ToolBody JSON. */
1030
- const SessionToolBodyChunk = object({
1031
- sessionId: string(),
1032
- toolCallId: string(),
1033
- data: string(),
1034
- totalBytes: number().int(),
1035
- nextOffset: number().int().optional()
1036
- });
1037
- //#endregion
1038
- //#region ../protocol/dist/frames/channel.js
1039
- /**
1040
- * Channel agent directory (D→C REQ → REP) — agent collaboration lookup.
1041
- *
1042
- * An agent asks (via a daemon-side tool) "who else is in this channel?". The
1043
- * daemon forwards it to the CP — the ONLY authority for the FULL channel roster,
1044
- * because agents in one channel may run on different daemons (each daemon knows
1045
- * only its own agents). The CP joins the channel's integrations to their agents
1046
- * and returns their public metadata (name / displayName / description / status).
1047
- * Metadata only — never message content (§1/§12).
1048
- *
1049
- * Direction is daemon→CP (like `auth` / `register`): the daemon sends the REQ and
1050
- * the CP replies with `channel/agents/ok` (corr = the req id).
1051
- */
1052
- /** One agent visible in a channel — the collaboration directory entry. */
1053
- const ChannelAgent = object({
1054
- agentId: string().uuid(),
1055
- name: string(),
1056
- displayName: string().optional(),
1057
- description: string().optional(),
1058
- status: _enum([
1059
- "active",
1060
- "inactive",
1061
- "paused"
1062
- ])
1063
- });
1064
- /** D→C REQ: list the agents in a channel (for the asking agent's collaboration tool). */
1065
- const ChannelAgentsReq = object({
1066
- platform: Platform,
1067
- channel: string()
1068
- });
1069
- /** C→D REP (corr = req id): every agent in the channel across all daemons. */
1070
- const ChannelAgentsOk = object({
1071
- platform: Platform,
1072
- channel: string(),
1073
- agents: array(ChannelAgent)
1074
- });
1075
- //#endregion
1076
- //#region ../protocol/dist/frames/workspace.js
1077
- /**
1078
- * Agent workspace file browsing (C→D REQ → REP) — the console's on-demand pulls.
1079
- *
1080
- * The CP stores NO workspace data — directory listings and file bytes are pulled
1081
- * live from the owning daemon and proxied to the console, never persisted
1082
- * (body-locality). Missing paths are DATA (`exists:false`), not errors; only
1083
- * path-containment violations (`BAD_PAYLOAD`) and unexpected fs failures
1084
- * (`INTERNAL`) come back as `error` frames.
1085
- *
1086
- * - `workspace/list`: one cursor-paginated page of a directory's entries.
1087
- * - `workspace/read`: one byte slice of a file. `limit` is only a ceiling — the
1088
- * daemon returns FEWER bytes when needed to keep the JSON-escaped REP under the
1089
- * 256 KiB frame cap (control bytes escape to `\uXXXX`, a 6× blowup), and always
1090
- * ends the slice on a UTF-8 character boundary. `nextOffset` is the authoritative
1091
- * byte offset to request next — callers must NOT recompute it from the decoded
1092
- * `content` (a split multi-byte char would make the byte count drift).
1093
- */
1094
- /** One directory entry in a workspace listing (name-only; not a full path). */
1095
- const WorkspaceEntry = object({
1096
- name: string(),
1097
- type: _enum([
1098
- "dir",
1099
- "file",
1100
- "symlink",
1101
- "other"
1102
- ]),
1103
- size: number().int().nonnegative().optional(),
1104
- mtime: string().optional()
1105
- });
1106
- /** C→D REQ: list one page of a directory in the agent's workspace. */
1107
- const WorkspaceListReq = object({
1108
- agentId: string().min(1),
1109
- path: string().default(""),
1110
- cursor: string().optional(),
1111
- limit: number().int().positive().max(500).default(200)
1112
- });
1113
- /** D→C REP (corr = the req id): a page of entries + the cursor for the next page. */
1114
- const WorkspaceListPage = object({
1115
- agentId: string(),
1116
- path: string(),
1117
- exists: boolean(),
1118
- entries: array(WorkspaceEntry),
1119
- nextCursor: string().optional()
1120
- });
1121
- /** C→D REQ: read one byte slice of a file in the agent's workspace. */
1122
- const WorkspaceReadReq = object({
1123
- agentId: string().min(1),
1124
- path: string().min(1),
1125
- offset: number().int().nonnegative().default(0),
1126
- limit: number().int().positive().max(65536).default(65536)
1127
- });
1128
- /** D→C REP (corr = the req id): the file slice (or `exists:false` / binary-detected). */
1129
- const WorkspaceReadContent = object({
1130
- agentId: string(),
1131
- path: string(),
1132
- exists: boolean(),
1133
- size: number().int().nonnegative().optional(),
1134
- mtime: string().optional(),
1135
- encoding: _enum(["utf8", "none"]).optional(),
1136
- content: string().optional(),
1137
- offset: number().int().nonnegative().optional(),
1138
- nextOffset: number().int().nonnegative().optional(),
1139
- truncated: boolean().optional()
1140
- });
1141
- /**
1142
- * Agent workspace git ops (C→D REQ → REP) — the console's on-demand controls for
1143
- * a git-repo workspace. Like list/read these run daemon-local; the CP proxies the
1144
- * outcome and stores nothing.
1145
- *
1146
- * These are DATA-oriented: a non-repo workspace (`isRepo:false`), a dirty tree,
1147
- * or a pull that can't fast-forward (offline, diverged, local edits) all come
1148
- * back as a normal REP — NOT an `error` frame. Only an unknown agent
1149
- * (`BAD_PAYLOAD`) or an unexpected git/fs failure (`INTERNAL`) is an error.
1150
- */
1151
- /** C→D REQ: report `git status` of the agent's workspace (is it clean?). */
1152
- const WorkspaceGitStatusReq = object({ agentId: string().min(1) });
1153
- /** One changed path in a `git status`. `index`/`workingDir` are git's per-file XY
1154
- * status chars (' ', 'M', 'A', 'D', 'R', 'C', 'U', '?', …). */
1155
- const WorkspaceGitFile = object({
1156
- path: string(),
1157
- index: string(),
1158
- workingDir: string()
1159
- });
1160
- /** The HEAD commit of the checkout. */
1161
- const WorkspaceGitCommit = object({
1162
- sha: string(),
1163
- shortSha: string(),
1164
- subject: string(),
1165
- committedAt: string()
1166
- });
1167
- /** D→C REP (corr = the req id): the working-tree status. */
1168
- const WorkspaceGitStatus = object({
1169
- agentId: string(),
1170
- isRepo: boolean(),
1171
- clean: boolean(),
1172
- branch: string().optional(),
1173
- tracking: string().optional(),
1174
- ahead: number().int().nonnegative().optional(),
1175
- behind: number().int().nonnegative().optional(),
1176
- files: array(WorkspaceGitFile).optional(),
1177
- truncated: boolean().optional(),
1178
- lastCommit: WorkspaceGitCommit.optional(),
1179
- lastFetchAt: string().optional()
1180
- });
1181
- /** C→D REQ: force a fast-forward-only `git pull` in the agent's workspace now. */
1182
- const WorkspaceGitPullReq = object({ agentId: string().min(1) });
1183
- /** D→C REP (corr = the req id): the pull outcome. A failed pull is DATA
1184
- * (`ok:false` + `detail`), not an error frame (see the section docblock). */
1185
- const WorkspaceGitPullResult = object({
1186
- agentId: string(),
1187
- isRepo: boolean(),
1188
- ok: boolean(),
1189
- detail: string().optional(),
1190
- changed: number().int().nonnegative().optional(),
1191
- insertions: number().int().nonnegative().optional(),
1192
- deletions: number().int().nonnegative().optional()
1193
- });
1194
- //#endregion
1195
- //#region ../protocol/dist/frames/memory.js
1196
- /**
1197
- * Agent memory directory (C→D REQ → REP) — the console's read/write of an agent's
1198
- * long-term memory, and the surface the daemon serves it from.
1199
- *
1200
- * Memory is a DIRECTORY at the agent's ROOT (`<agent-root>/memory/`), OUTSIDE the
1201
- * workspace — so it survives a workspace reset / re-clone and is never committed
1202
- * into a github-workspace repo. It follows the index-plus-topics shape: a lean
1203
- * `MEMORY.md` index (injected into the prompt every session, bounded) plus any
1204
- * number of `<topic>.md` files the agent reads on demand. The agent maintains it
1205
- * via MCP tools; these frames are the third channel — the CP reading/writing it
1206
- * for the console — all pointing at the same directory.
1207
- *
1208
- * The CP stores NO memory content — bytes are pulled live from the owning daemon
1209
- * and proxied to the console, never persisted (body-locality). A not-yet-created
1210
- * file/dir is DATA (`exists:false`), not an error; only an unknown agent or a
1211
- * path-containment violation (`BAD_PAYLOAD`) or an unexpected fs failure
1212
- * (`INTERNAL`) comes back as an `error` frame.
1213
- *
1214
- * - `memory/list`: the topic files in the memory dir (flat; the index + topics).
1215
- * - `memory/read`: one byte slice of a memory file (`path` relative to the memory
1216
- * dir; default `MEMORY.md`). `limit` is a ceiling — the daemon returns fewer
1217
- * bytes to keep the JSON-escaped REP under the 256 KiB frame cap and always ends
1218
- * on a UTF-8 boundary. `nextOffset` is authoritative — do NOT recompute it.
1219
- * - `memory/write`: replace the whole named memory file with `content`.
1220
- */
1221
- /** The default file name of the memory index (loaded into the prompt each session). */
1222
- const MEMORY_INDEX = "MEMORY.md";
1223
- /** One entry in a memory-dir listing (name-only; not a path). */
1224
- const MemoryEntry = object({
1225
- name: string(),
1226
- size: number().int().nonnegative(),
1227
- mtime: string()
1228
- });
1229
- /** C→D REQ: list the files in the agent's memory dir. */
1230
- const MemoryListReq = object({ agentId: string().min(1) });
1231
- /** D→C REP (corr = the req id): the memory dir's files (or `exists:false`). */
1232
- const MemoryListPage = object({
1233
- agentId: string(),
1234
- exists: boolean(),
1235
- entries: array(MemoryEntry)
1236
- });
1237
- /** C→D REQ: read one byte slice of a memory file (path relative to the memory dir). */
1238
- const MemoryReadReq = object({
1239
- agentId: string().min(1),
1240
- path: string().default(MEMORY_INDEX),
1241
- offset: number().int().nonnegative().default(0),
1242
- limit: number().int().positive().max(65536).default(65536)
1243
- });
1244
- /** D→C REP (corr = the req id): the memory file slice (or `exists:false`). Always utf8 text. */
1245
- const MemoryReadContent = object({
1246
- agentId: string(),
1247
- path: string(),
1248
- exists: boolean(),
1249
- size: number().int().nonnegative().optional(),
1250
- mtime: string().optional(),
1251
- content: string().optional(),
1252
- offset: number().int().nonnegative().optional(),
1253
- nextOffset: number().int().nonnegative().optional(),
1254
- truncated: boolean().optional()
1255
- });
1256
- /** C→D REQ: replace the whole named memory file with `content` (console edit). */
1257
- const MemoryWriteReq = object({
1258
- agentId: string().min(1),
1259
- path: string().default(MEMORY_INDEX),
1260
- content: string(),
1261
- ifMatchMtime: string().optional()
1262
- });
1263
- /** D→C REP (corr = the req id): the written state, so the console can refresh. */
1264
- const MemoryWriteOk = object({
1265
- agentId: string(),
1266
- path: string(),
1267
- size: number().int().nonnegative(),
1268
- mtime: string()
1269
- });
1270
- //#endregion
1271
- //#region ../protocol/dist/frames/telemetry.js
1272
- /**
1273
- * Telemetry & facts (D→C) — protocol §7.
1274
- *
1275
- * The dashboard's real-time source. `event/session` is the converged milestone
1276
- * feed (NOT the message stream); `facts/daemon-runtimes` is the observed runtime
1277
- * capability feed; `heartbeat` drives the watchdog.
1278
- */
1279
- const Heartbeat = object({
1280
- load: object({
1281
- cpu: number(),
1282
- mem: number(),
1283
- agents: number().int()
1284
- }),
1285
- health: _enum(["ok", "degraded"]),
1286
- activeSessions: number().int(),
1287
- degradedScopes: array(string()).default([])
1288
- });
1289
- /** Converged session lifecycle milestone — protocol §7.2. NOT the message stream. */
1290
- const EventSession = object({
1291
- sessionId: string(),
1292
- agentId: string().uuid(),
1293
- launchId: string().uuid().optional(),
1294
- phase: _enum([
1295
- "start",
1296
- "plan",
1297
- "problem",
1298
- "end"
1299
- ]),
1300
- platform: Platform.optional(),
1301
- channel: string().optional(),
1302
- thread: string().optional(),
1303
- link: string().optional(),
1304
- summary: string().optional(),
1305
- title: string().optional(),
1306
- status: string().optional(),
1307
- lastActivityAt: string().datetime().optional(),
1308
- triggeredBy: string().optional(),
1309
- channelName: string().optional(),
1310
- triggeredByName: string().optional(),
1311
- threadUrl: string().optional(),
1312
- ts: string().datetime()
1313
- });
1314
- /**
1315
- * Per-session token-usage report — D→C EVT. The daemon meters usage from the
1316
- * agent's ACP stream and reports the session's CUMULATIVE snapshot (latest-wins)
1317
- * so the CP can persist it for the console's historical usage aggregates. This is
1318
- * dashboard metadata (token counts + cost), never the message stream. Idempotent
1319
- * by `sessionId`: re-sending the same snapshot is a no-op upsert on the CP.
1320
- */
1321
- const UsageReport = object({
1322
- sessionId: string(),
1323
- agentId: string().uuid(),
1324
- platform: string().optional(),
1325
- channel: string().optional(),
1326
- lastActivityAt: string(),
1327
- usage: SessionUsage
1328
- });
1329
- /**
1330
- * Observed capabilities of an installed runtime — protocol §7.3. The element
1331
- * type of the `facts/daemon-runtimes` snapshot. As a standalone per-runtime
1332
- * frame (`facts/runtime-profile`, upsert-only) it is DEPRECATED: current
1333
- * daemons report the full snapshot instead; the CP keeps accepting the old
1334
- * frame from older daemons.
1335
- */
1336
- /**
1337
- * MCP transports a runtime accepts on `session/new` `mcpServers` (from the ACP
1338
- * `initialize` response's `agentCapabilities.mcpCapabilities`). Stdio is the ACP
1339
- * baseline every agent must accept, so only the optional transports are listed.
1340
- */
1341
- const McpTransportCapabilities = object({
1342
- http: boolean(),
1343
- sse: boolean()
1344
- });
1345
- const FactsRuntimeProfile = object({
1346
- runtime: string(),
1347
- version: string(),
1348
- models: array(string()),
1349
- contextWindow: number().int().optional(),
1350
- acpSupport: _enum([
1351
- "full",
1352
- "partial",
1353
- "none"
1354
- ]),
1355
- acpProtocolVersion: number().int().optional(),
1356
- toolCalling: boolean(),
1357
- mcpCapabilities: McpTransportCapabilities.optional()
1358
- });
1359
- /**
1360
- * Probe result for one daemon-configured MCP server (daemon config `mcpServers`).
1361
- * The daemon connects to the server (MCP initialize + `tools/list`) so the
1362
- * console can show what each server offers before the user enables it on an
1363
- * agent. Metadata only — definitions (commands, URLs, headers) stay daemon-local.
1364
- */
1365
- const FactsMcpServer = object({
1366
- name: string(),
1367
- transport: _enum([
1368
- "stdio",
1369
- "http",
1370
- "sse"
1371
- ]),
1372
- /** True iff the MCP handshake succeeded. */
1373
- ok: boolean(),
1374
- tools: array(string()).default([]),
1375
- version: string().optional(),
1376
- error: string().optional()
1377
- });
1378
- /**
1379
- * Full snapshot of the daemon's installed runtimes — D→C EVT (protocol §7.3a),
1380
- * emitted on each register and again once the background probe sweep completes
1381
- * (with the learned `models[]`). REPLACE semantics: the CP reconciles its
1382
- * stored runtime list for the daemon to exactly `runtimes[]`, pruning runtimes
1383
- * that are no longer installed. Idempotent (latest-wins). Supersedes the
1384
- * per-runtime `facts/runtime-profile` upsert.
1385
- */
1386
- const DaemonRuntimes = object({
1387
- runtimes: array(FactsRuntimeProfile),
1388
- mcpServers: array(FactsMcpServer).default([])
1389
- });
1390
- /** Non-secret config update — C→D EVT, protocol §8.3 / frame #26. */
1391
- const ConfigPush = object({ keys: record(string(), unknown()) });
1392
- /** Fleet: drain+exit, supervisor restarts — C→D REQ, protocol §8.3 / frame #27. */
1393
- const DaemonRestart = object({
1394
- reason: string(),
1395
- drainFirst: boolean().default(true)
1396
- });
1397
- /** Fleet: drain+exit for version bump — C→D REQ, protocol §8.3 / frame #28. */
1398
- const DaemonUpgrade = object({
1399
- targetVersion: string(),
1400
- drainFirst: boolean().default(true)
1401
- });
1402
- /** Reply to a fleet restart/upgrade REQ (protocol §8.3). */
1403
- const DaemonControlAck = object({
1404
- accepted: boolean(),
1405
- willDrainUntil: string().datetime().optional()
1406
- });
1407
- /**
1408
- * Generic acknowledgement for REQs whose reply is just `ack` in the frame index
1409
- * (cron/upsert, cron/remove, agent/stop). Kept permissive but typed.
1410
- */
1411
- const Ack = object({
1412
- ok: boolean(),
1413
- reason: string().optional()
1414
- });
1415
- const ErrorFrame = object({
1416
- code: _enum([
1417
- "UNKNOWN_FRAME",
1418
- "FRAME_TOO_LARGE",
1419
- "PROTOCOL_STATE",
1420
- "BAD_PAYLOAD",
1421
- "AUTH_FAILED",
1422
- "ATTESTATION_INVALID",
1423
- "STALE_EPOCH",
1424
- "STALE_LAUNCH",
1425
- "NO_SESSION",
1426
- "SCOPE_DENIED",
1427
- "LEASE_EXPIRED",
1428
- "LEASE_DENIED",
1429
- "RATE_LIMITED",
1430
- "CONFLICT",
1431
- "INTERNAL"
1432
- ]),
1433
- message: string(),
1434
- retryable: boolean(),
1435
- details: record(string(), unknown()).optional()
1436
- });
1437
- object({
1438
- accepted: boolean(),
1439
- turnId: string().uuid(),
1440
- reason: _enum([
1441
- "queued",
1442
- "no_agent",
1443
- "busy",
1444
- "paused",
1445
- "draining"
1446
- ]).optional()
1447
- });
1448
- const WebchatEvent = discriminatedUnion("kind", [
1449
- object({
1450
- kind: literal("message"),
1451
- text: string()
1452
- }),
1453
- object({
1454
- kind: literal("thinking"),
1455
- text: string()
1456
- }),
1457
- object({
1458
- kind: literal("tool_call"),
1459
- toolCallId: string(),
1460
- title: string(),
1461
- status: string()
1462
- }),
1463
- object({
1464
- kind: literal("tool_update"),
1465
- toolCallId: string(),
1466
- status: string()
1467
- }),
1468
- object({
1469
- kind: literal("session_info"),
1470
- title: string()
1471
- })
1472
- ]);
1473
- const WebchatStatus = object({
1474
- model: string().optional(),
1475
- effort: string().optional(),
1476
- permissionMode: string().optional(),
1477
- fastMode: boolean().optional(),
1478
- contextUsed: number().int().optional(),
1479
- contextSize: number().int().optional(),
1480
- totalTokens: number().int().optional(),
1481
- costAmount: number().optional(),
1482
- costCurrency: string().optional(),
1483
- models: array(string()).optional(),
1484
- efforts: array(string()).optional(),
1485
- permissionModes: array(string()).optional(),
1486
- fastModeAvailable: boolean().optional(),
1487
- sessionId: string().optional()
1488
- });
1489
- const WebchatOutput = object({
1490
- conversationId: string().uuid(),
1491
- turnId: string().uuid(),
1492
- index: number().int(),
1493
- event: WebchatEvent.optional(),
1494
- status: WebchatStatus.optional()
1495
- }).refine((o) => o.event !== void 0 || o.status !== void 0, { message: "WebchatOutput must carry event and/or status" });
1496
- const WebchatDone = object({
1497
- conversationId: string().uuid(),
1498
- turnId: string().uuid(),
1499
- stopReason: string().optional(),
1500
- usage: object({
1501
- used: number().int().optional(),
1502
- cost: number().optional()
1503
- }).optional(),
1504
- error: string().optional()
1505
- });
1506
- //#endregion
1507
- //#region ../protocol/dist/frame.js
1508
- /**
1509
- * The single source of truth for the wire: `type` string → payload zod schema.
1510
- *
1511
- * Mirrors the frame index in daemon-cp-ws-protocol.md §10 (the 29 numbered
1512
- * frames) plus the correlated REP types named in the "Reply" column
1513
- * (`route/assign/ack`, `drain/done`, and the generic `ack` replies) that also
1514
- * travel on the wire and must be decodable.
1515
- *
1516
- * `ws/codec.ts` validates every inbound `payload` against `FRAME_SCHEMAS[type]`;
1517
- * an unknown `type` → `ErrorFrame{code:"UNKNOWN_FRAME"}` (a REP, not a close).
1518
- */
1519
- const FRAME_SCHEMAS = {
1520
- auth: AuthReq,
1521
- "auth/ok": AuthOk,
1522
- register: RegisterReq,
1523
- "register/ok": RegisterOk,
1524
- "relay/roster": RelayRosterUpdate,
1525
- heartbeat: Heartbeat,
1526
- "agent/launch": AgentLaunch,
1527
- "agent/launched": AgentLaunched,
1528
- "agent/stop": AgentStop,
1529
- "agent/upsert": AgentUpsert,
1530
- "agent/remove": AgentRemove,
1531
- "agent/activity": AgentActivity,
1532
- "agent/scope-denied": AgentScopeDenied,
1533
- "route/assign": RouteAssign,
1534
- "route/assign/ack": RouteAssignAck,
1535
- "route/update": RouteUpdate,
1536
- "daemon/drain": Drain,
1537
- "drain/progress": DrainProgress,
1538
- "drain/done": DrainDone,
1539
- "cron/upsert": CronUpsert,
1540
- "cron/remove": CronRemove,
1541
- "cron/report": CronReport,
1542
- "cron/run": CronRunNow,
1543
- "integration/upsert": IntegrationUpsert,
1544
- "integration/remove": IntegrationRemove,
1545
- "integration/channels": IntegrationChannels,
1546
- "gitcred/request": GitCredRequest,
1547
- "gitcred/grant": GitCredGrant,
1548
- "secrets/request": SecretsRequest,
1549
- "secrets/grant": SecretsGrant,
1550
- "secrets/renew": SecretsRenew,
1551
- "secrets/revoke": SecretsRevoke,
1552
- "scope-attestation": ScopeAttestation,
1553
- "event/session": EventSession,
1554
- "usage/report": UsageReport,
1555
- "facts/runtime-profile": FactsRuntimeProfile,
1556
- "facts/daemon-runtimes": DaemonRuntimes,
1557
- "session/list": SessionListReq,
1558
- "session/list/page": SessionListPage,
1559
- "session/history": SessionHistoryReq,
1560
- "session/history/page": SessionHistoryPage,
1561
- "session/tool-body": SessionToolBodyReq,
1562
- "session/tool-body/chunk": SessionToolBodyChunk,
1563
- "channel/agents": ChannelAgentsReq,
1564
- "channel/agents/ok": ChannelAgentsOk,
1565
- "workspace/list": WorkspaceListReq,
1566
- "workspace/list/page": WorkspaceListPage,
1567
- "workspace/read": WorkspaceReadReq,
1568
- "workspace/read/content": WorkspaceReadContent,
1569
- "workspace/gitstatus": WorkspaceGitStatusReq,
1570
- "workspace/gitstatus/result": WorkspaceGitStatus,
1571
- "workspace/gitpull": WorkspaceGitPullReq,
1572
- "workspace/gitpull/result": WorkspaceGitPullResult,
1573
- "memory/list": MemoryListReq,
1574
- "memory/list/page": MemoryListPage,
1575
- "memory/read": MemoryReadReq,
1576
- "memory/read/content": MemoryReadContent,
1577
- "memory/write": MemoryWriteReq,
1578
- "memory/write/ok": MemoryWriteOk,
1579
- "config/push": ConfigPush,
1580
- "daemon/restart": DaemonRestart,
1581
- "daemon/upgrade": DaemonUpgrade,
1582
- "daemon/control/ack": DaemonControlAck,
1583
- ack: Ack,
1584
- error: ErrorFrame
1585
- };
1586
- Object.keys(FRAME_SCHEMAS);
1587
- /**
1588
- * Builds the envelope schema for one frame `type` with a `type` literal and the
1589
- * typed payload, so the discriminated union infers `payload` precisely.
1590
- *
1591
- * Kept as a local generic (payload pinned to `FRAME_SCHEMAS[T]`) so a
1592
- * mispaired member — `frame('auth', FRAME_SCHEMAS['register'])` — is a compile
1593
- * error; a bare `frameSchema` alias would accept it. The body is deliberately
1594
- * inlined rather than delegated: `return frameSchema(type, payload)` under
1595
- * THIS generic signature collapses every union member's `payload` to `unknown`
1596
- * (113 × TS18046 under TS 6.0.3 / zod 4.4.3 — reproducible by making exactly
1597
- * that substitution), while the small relay unions call `frameSchema` with
1598
- * concrete schemas and infer fine.
1599
- */
1600
- function frame(type, payload) {
1601
- return object({
1602
- v: literal(1),
1603
- id: string().uuid(),
1604
- ts: string().datetime(),
1605
- type: literal(type),
1606
- corr: string().uuid().optional(),
1607
- payload
1608
- });
1609
- }
1610
- discriminatedUnion("type", [
1611
- frame("auth", FRAME_SCHEMAS["auth"]),
1612
- frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
1613
- frame("register", FRAME_SCHEMAS["register"]),
1614
- frame("register/ok", FRAME_SCHEMAS["register/ok"]),
1615
- frame("relay/roster", FRAME_SCHEMAS["relay/roster"]),
1616
- frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
1617
- frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
1618
- frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
1619
- frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
1620
- frame("agent/upsert", FRAME_SCHEMAS["agent/upsert"]),
1621
- frame("agent/remove", FRAME_SCHEMAS["agent/remove"]),
1622
- frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
1623
- frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
1624
- frame("route/assign", FRAME_SCHEMAS["route/assign"]),
1625
- frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
1626
- frame("route/update", FRAME_SCHEMAS["route/update"]),
1627
- frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
1628
- frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
1629
- frame("drain/done", FRAME_SCHEMAS["drain/done"]),
1630
- frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
1631
- frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
1632
- frame("cron/report", FRAME_SCHEMAS["cron/report"]),
1633
- frame("cron/run", FRAME_SCHEMAS["cron/run"]),
1634
- frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
1635
- frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
1636
- frame("integration/channels", FRAME_SCHEMAS["integration/channels"]),
1637
- frame("gitcred/request", FRAME_SCHEMAS["gitcred/request"]),
1638
- frame("gitcred/grant", FRAME_SCHEMAS["gitcred/grant"]),
1639
- frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
1640
- frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
1641
- frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
1642
- frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
1643
- frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
1644
- frame("event/session", FRAME_SCHEMAS["event/session"]),
1645
- frame("usage/report", FRAME_SCHEMAS["usage/report"]),
1646
- frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
1647
- frame("facts/daemon-runtimes", FRAME_SCHEMAS["facts/daemon-runtimes"]),
1648
- frame("session/list", FRAME_SCHEMAS["session/list"]),
1649
- frame("session/list/page", FRAME_SCHEMAS["session/list/page"]),
1650
- frame("session/history", FRAME_SCHEMAS["session/history"]),
1651
- frame("session/history/page", FRAME_SCHEMAS["session/history/page"]),
1652
- frame("session/tool-body", FRAME_SCHEMAS["session/tool-body"]),
1653
- frame("session/tool-body/chunk", FRAME_SCHEMAS["session/tool-body/chunk"]),
1654
- frame("channel/agents", FRAME_SCHEMAS["channel/agents"]),
1655
- frame("channel/agents/ok", FRAME_SCHEMAS["channel/agents/ok"]),
1656
- frame("workspace/list", FRAME_SCHEMAS["workspace/list"]),
1657
- frame("workspace/list/page", FRAME_SCHEMAS["workspace/list/page"]),
1658
- frame("workspace/read", FRAME_SCHEMAS["workspace/read"]),
1659
- frame("workspace/read/content", FRAME_SCHEMAS["workspace/read/content"]),
1660
- frame("workspace/gitstatus", FRAME_SCHEMAS["workspace/gitstatus"]),
1661
- frame("workspace/gitstatus/result", FRAME_SCHEMAS["workspace/gitstatus/result"]),
1662
- frame("workspace/gitpull", FRAME_SCHEMAS["workspace/gitpull"]),
1663
- frame("workspace/gitpull/result", FRAME_SCHEMAS["workspace/gitpull/result"]),
1664
- frame("memory/list", FRAME_SCHEMAS["memory/list"]),
1665
- frame("memory/list/page", FRAME_SCHEMAS["memory/list/page"]),
1666
- frame("memory/read", FRAME_SCHEMAS["memory/read"]),
1667
- frame("memory/read/content", FRAME_SCHEMAS["memory/read/content"]),
1668
- frame("memory/write", FRAME_SCHEMAS["memory/write"]),
1669
- frame("memory/write/ok", FRAME_SCHEMAS["memory/write/ok"]),
1670
- frame("config/push", FRAME_SCHEMAS["config/push"]),
1671
- frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
1672
- frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
1673
- frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
1674
- frame("ack", FRAME_SCHEMAS["ack"]),
1675
- frame("error", FRAME_SCHEMAS["error"])
1676
- ]);
1677
- //#endregion
1678
- //#region ../protocol/dist/wire.js
1679
- /**
1680
- * Generic envelope codec — the schema-map-agnostic core shared by every wire.
1681
- *
1682
- * The daemon↔CP wire (`codec.ts` over `FRAME_SCHEMAS`) and the two relay wires
1683
- * (`frames/relay-cp.ts` / `frames/relay-daemon.ts` over their OWN maps) all
1684
- * decode the same envelope shape but accept disjoint frame families — a relay
1685
- * socket must answer a daemon↔CP frame with `UNKNOWN_FRAME`, and vice versa
1686
- * (shared-bot-relay.md §8 "独立帧 union,不混入 daemon 协议").
1687
- */
1688
- /** Soft cap per frame — 256 KiB (protocol §1). Over this → FRAME_TOO_LARGE. */
1689
- const MAX_FRAME_BYTES = 256 * 1024;
1690
- const textEncoder = new TextEncoder();
1691
- function byteLength(text) {
1692
- return textEncoder.encode(text).length;
1693
- }
1694
- function extractControlExt(json) {
1695
- if (typeof json !== "object" || json === null) return void 0;
1696
- const o = json;
1697
- const ext = {};
1698
- if (typeof o.epoch === "number") ext.epoch = o.epoch;
1699
- if (typeof o.agentId === "string") ext.agentId = o.agentId;
1700
- if (typeof o.launchId === "string") ext.launchId = o.launchId;
1701
- return Object.keys(ext).length > 0 ? ext : void 0;
1702
- }
1703
- /**
1704
- * Decode one wire frame against `schemas` (the wire's `type` → payload map).
1705
- * Two-step parse: envelope first, then the per-type payload schema — an unknown
1706
- * `type` is a typed `UNKNOWN_FRAME` failure (a REP, never a hard close).
1707
- */
1708
- function decodeEnvelopeWith(schemas, text) {
1709
- if (byteLength(text) > 262144) return {
1710
- ok: false,
1711
- id: NIL_UUID,
1712
- msg: "FRAME_TOO_LARGE"
1713
- };
1714
- let json;
1715
- try {
1716
- json = JSON.parse(text);
1717
- } catch {
1718
- return {
1719
- ok: false,
1720
- id: NIL_UUID,
1721
- msg: "invalid json"
1722
- };
1723
- }
1724
- const env = Envelope.safeParse(json);
1725
- if (!env.success) return {
1726
- ok: false,
1727
- id: typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID,
1728
- msg: env.error.message
1729
- };
1730
- const schema = schemas[env.data.type];
1731
- if (!schema) return {
1732
- ok: false,
1733
- id: env.data.id,
1734
- msg: "UNKNOWN_FRAME"
1735
- };
1736
- const payload = schema.safeParse(env.data.payload);
1737
- if (!payload.success) return {
1738
- ok: false,
1739
- id: env.data.id,
1740
- msg: payload.error.message
1741
- };
1742
- const ext = extractControlExt(json);
1743
- return {
1744
- ok: true,
1745
- frame: {
1746
- ...env.data,
1747
- payload: payload.data
1748
- },
1749
- ...ext ? { ext } : {}
1750
- };
1751
- }
1752
- /**
1753
- * Wrap a payload in the wire envelope. Untyped core — each wire exports a typed
1754
- * wrapper (`buildEnvelope` / `buildRelayCpFrame` / `buildRelayDaemonFrame`) that
1755
- * pins `type` to its own union and casts the result.
1756
- */
1757
- function buildEnvelopeRaw(type, payload, opts = {}) {
1758
- return {
1759
- v: 1,
1760
- id: opts.id ?? randomUUID(),
1761
- ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1762
- type,
1763
- payload,
1764
- ...opts.corr ? { corr: opts.corr } : {},
1765
- ...opts.ext ?? {}
1766
- };
1767
- }
1768
- //#endregion
1769
- //#region ../protocol/dist/codec.js
1770
- function decodeEnvelope(text) {
1771
- return decodeEnvelopeWith(FRAME_SCHEMAS, text);
1772
- }
1773
- function buildEnvelope(type, payload, opts = {}) {
1774
- return buildEnvelopeRaw(type, payload, opts);
1775
- }
1776
- function encode(frame) {
1777
- return JSON.stringify(frame);
1778
- }
1779
- //#endregion
1780
- //#region ../protocol/dist/frames/relay-daemon.js
1781
- /**
1782
- * relay↔daemon frames (`rd/*`) — shared-bot-relay.md §7.2.
1783
- *
1784
- * The daemon dials OUT to every relay in its roster (one WS per (daemon, relay)
1785
- * pair, multiplexing every source). This wire CARRIES MESSAGE CONTENT — it is
1786
- * the ingress data plane — so nothing on it is ever logged with its payload.
1787
- *
1788
- * Direction per frame:
1789
- * - `rd/hello` (+ `rd/hello/ok`) D→R REQ / R→D REP — authenticate the dial-in
1790
- * - `rd/msg` R→D REQ — one inbound item, already routed
1791
- * - `rd/ack` D→R REP — receipt (+ webchat turn verdict)
1792
- * - `rd/chat` D→R EVT — webchat output stream back to the
1793
- * browser (webchat is the one BIDIRECTIONAL
1794
- * source: its far end hangs off the relay)
1795
- *
1796
- * Milestone A carries the webchat slice only; `im` (shared bot) and `hook`
1797
- * (webhook) members join the `rd/msg` union with milestone B.
1798
- */
1799
- /** Subprotocol negotiated on the relay↔daemon socket. */
1800
- const RELAY_DAEMON_SUBPROTOCOL = "agentconnect.rd.v1";
1801
- /** The canonical path the relay mounts its daemon-facing `rd/*` server at, and
1802
- * the daemon appends to a roster `daemonUrl` origin when dialing (mirrors the
1803
- * daemon↔CP `/daemon/ws` convention). A roster url already ending in it is used
1804
- * verbatim. */
1805
- const RELAY_DAEMON_WS_PATH = "/rd/ws";
1806
- const RdHello = object({
1807
- apiKey: string().min(1),
1808
- daemonId: string().uuid()
1809
- });
1810
- const RdHelloOk = object({ relayId: string().uuid() });
1811
- const RelayWebchatOp = discriminatedUnion("op", [
1812
- object({
1813
- op: literal("turn"),
1814
- text: string(),
1815
- user: string().optional()
1816
- }),
1817
- object({
1818
- op: literal("set_model"),
1819
- model: string()
1820
- }),
1821
- object({
1822
- op: literal("set_effort"),
1823
- effort: string()
1824
- }),
1825
- object({
1826
- op: literal("set_permission_mode"),
1827
- permissionMode: string()
1828
- }),
1829
- object({
1830
- op: literal("set_fast"),
1831
- fastMode: boolean()
1832
- }),
1833
- object({ op: literal("cancel") }),
1834
- object({ op: literal("close") })
1835
- ]);
1836
- const RdMsg = discriminatedUnion("source", [object({
1837
- source: literal("webchat"),
1838
- agentId: string().uuid(),
1839
- sessionKey: string().min(1),
1840
- msgId: string().min(1),
1841
- chatId: string().uuid(),
1842
- payload: RelayWebchatOp
1843
- })]);
1844
- const RdAck = object({
1845
- msgId: string(),
1846
- accepted: boolean(),
1847
- turnId: string().uuid().optional(),
1848
- reason: string().optional()
1849
- });
1850
- const RdChatEvent = discriminatedUnion("kind", [object({
1851
- kind: literal("output"),
1852
- output: WebchatOutput
1853
- }), object({
1854
- kind: literal("done"),
1855
- done: WebchatDone
1856
- })]);
1857
- /** `type` string → payload schema for the relay↔daemon wire. */
1858
- const RELAY_DAEMON_SCHEMAS = {
1859
- "rd/hello": RdHello,
1860
- "rd/hello/ok": RdHelloOk,
1861
- "rd/msg": RdMsg,
1862
- "rd/ack": RdAck,
1863
- "rd/chat": object({
1864
- chatId: string().uuid(),
1865
- seq: number().int(),
1866
- event: RdChatEvent
1867
- }),
1868
- error: ErrorFrame
1869
- };
1870
- Object.keys(RELAY_DAEMON_SCHEMAS);
1871
- discriminatedUnion("type", [
1872
- frameSchema("rd/hello", RELAY_DAEMON_SCHEMAS["rd/hello"]),
1873
- frameSchema("rd/hello/ok", RELAY_DAEMON_SCHEMAS["rd/hello/ok"]),
1874
- frameSchema("rd/msg", RELAY_DAEMON_SCHEMAS["rd/msg"]),
1875
- frameSchema("rd/ack", RELAY_DAEMON_SCHEMAS["rd/ack"]),
1876
- frameSchema("rd/chat", RELAY_DAEMON_SCHEMAS["rd/chat"]),
1877
- frameSchema("error", RELAY_DAEMON_SCHEMAS["error"])
1878
- ]);
1879
- /** Decode one relay↔daemon wire frame (envelope + typed payload). */
1880
- function decodeRelayDaemonFrame(text) {
1881
- return decodeEnvelopeWith(RELAY_DAEMON_SCHEMAS, text);
1882
- }
1883
- /** Build a relay↔daemon frame with a compile-time-typed payload. */
1884
- function buildRelayDaemonFrame(type, payload, opts = {}) {
1885
- return buildEnvelopeRaw(type, payload, opts);
1886
- }
1887
- //#endregion
1888
- //#region ../protocol/dist/git-url.js
1889
- /**
1890
- * Git repo address helpers, shared by the CP (normalize on write) and the
1891
- * daemon (defensive normalize before clone).
1892
- *
1893
- * STORAGE INVARIANT: `workspace.gitRepo` is persisted as a FULL cloneable git
1894
- * address (e.g. `https://github.com/acme/infra`), never the `acme/infra`
1895
- * shorthand a user may type. UIs shorten it back to `org/repo` for display
1896
- * with `gitRepoLabel`.
1897
- */
1898
- /** Scheme-full address: https://, ssh://, git://, file://, … */
1899
- const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
1900
- /** scp-like ssh address: git@github.com:acme/infra(.git) */
1901
- const SCP_RE = /^[\w.-]+@[\w.-]+:(.+)$/;
1902
- /**
1903
- * Normalize a user-supplied repo reference into a full cloneable git address.
1904
- * Idempotent — full addresses pass through unchanged.
1905
- *
1906
- * - `https://…` / `ssh://…` / `git://…` / `git@host:…` → as-is
1907
- * - `github.com/acme/infra` (host-prefixed shorthand) → `https://github.com/acme/infra`
1908
- * - `acme/infra` (bare org/repo) → `https://github.com/acme/infra`
1909
- * - anything else (e.g. a local path) → as-is
1910
- */
1911
- function normalizeGitUrl(input) {
1912
- const s = input.trim().replace(/\/+$/, "");
1913
- if (!s || SCHEME_RE.test(s) || SCP_RE.test(s)) return s;
1914
- const segments = s.split("/");
1915
- if (segments.length >= 3 && segments[0].includes(".")) return `https://${s}`;
1916
- if (segments.length === 2 && segments.every((p) => p.length > 0)) return `https://github.com/${s}`;
1917
- return s;
1918
- }
1919
- //#endregion
1920
- //#region ../protocol/dist/index.js
1921
- /**
1922
- * Narrowing guard factory: `isFrame("auth")(frame)` narrows a decoded
1923
- * `AnyFrame` to the member whose `type` matches.
1924
- */
1925
- function isFrame(type) {
1926
- return (frame) => frame.type === type;
1927
- }
1928
- //#endregion
1929
374
  //#region src/agents/memory.ts
1930
375
  /**
1931
376
  * Agent memory — a DIRECTORY at the agent's ROOT (`<agent-root>/memory/`), OUTSIDE
@@ -1950,6 +395,14 @@ const MAX_INDEX_INJECT_BYTES = 25e3;
1950
395
  /** Hard cap on a single memory file. Injected/read every session, so an unbounded
1951
396
  * file would swell the prompt and the wire; writes over this are rejected. */
1952
397
  const MAX_MEMORY_FILE_BYTES = 256e3;
398
+ /** The append-only change log for an agent's memory, a sidecar beside the memory
399
+ * files (`<agent-root>/memory/.history`). One JSON line per write, for provenance
400
+ * (who/what/when changed a file). Dotfile so it never surfaces as a topic. */
401
+ const MEMORY_HISTORY_FILENAME = ".history";
402
+ /** Cap on a `before`/`after` snapshot stored in a history line — keeps a single log
403
+ * entry bounded even for a large file. Over this, the snapshot is truncated (with a
404
+ * `…` marker) rather than omitted, so the line stays small but still human-readable. */
405
+ const MAX_HISTORY_VALUE_BYTES = 4e3;
1953
406
  /** Raised when a memory path escapes the memory dir. Surfaces as `BAD_PAYLOAD`. */
1954
407
  var MemoryPathError = class extends Error {
1955
408
  constructor(message) {
@@ -2013,17 +466,47 @@ async function readMemoryFile(agentDir, relPath) {
2013
466
  throw err;
2014
467
  }
2015
468
  }
469
+ /** Truncate a snapshot to the history value cap, on a UTF-8 boundary, flagging it. */
470
+ function clampHistoryValue(text) {
471
+ if (Buffer.byteLength(text) <= 4e3) return {
472
+ value: text,
473
+ truncated: false
474
+ };
475
+ return {
476
+ value: Buffer.from(text, "utf8").subarray(0, MAX_HISTORY_VALUE_BYTES).toString("utf8") + "…",
477
+ truncated: true
478
+ };
479
+ }
480
+ /** Append one line to the memory change log (`<agent-root>/memory/.history`). A single
481
+ * `O_APPEND` write, so concurrent appends never interleave a partial line. Best-effort:
482
+ * a logging failure must never fail the write it describes, so errors are swallowed. */
483
+ async function appendHistory(agentDir, record) {
484
+ try {
485
+ await promises.mkdir(memoryDir(agentDir), { recursive: true });
486
+ await promises.appendFile(join(memoryDir(agentDir), MEMORY_HISTORY_FILENAME), JSON.stringify(record) + "\n", "utf8");
487
+ } catch {}
488
+ }
2016
489
  /** Overwrite a memory file with `content` (creating the dir if needed). Atomic
2017
- * (tmp + rename) so a concurrent read never sees a partial file.
490
+ * (tmp + rename) so a concurrent read never sees a partial file. Appends a line to
491
+ * the change log (`.history`) recording the add/update, its `before`/`after`
492
+ * snapshot (bounded), and the `source`.
2018
493
  *
2019
494
  * - Rejects content over {@link MAX_MEMORY_FILE_BYTES} ({@link MemoryTooLargeError}).
2020
495
  * - `ifMatchMtime` (optimistic concurrency): when given, the current file's mtime
2021
496
  * must equal it, else {@link MemoryConflictError} — so a console edit can't
2022
497
  * clobber a newer write. A brand-new file (no mtime) matches `ifMatchMtime`
2023
498
  * only when the caller passes none (or the empty string). */
2024
- async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime) {
499
+ async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime, source = "tool") {
2025
500
  if (Buffer.byteLength(content) > 256e3) throw new MemoryTooLargeError(`memory file exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`);
2026
501
  const abs = resolveInMemoryDir(agentDir, relPath);
502
+ let before = "";
503
+ let existed = false;
504
+ try {
505
+ before = await promises.readFile(abs, "utf8");
506
+ existed = true;
507
+ } catch (err) {
508
+ if (err.code !== "ENOENT") throw err;
509
+ }
2027
510
  if (ifMatchMtime) {
2028
511
  let current = null;
2029
512
  try {
@@ -2038,6 +521,18 @@ async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime) {
2038
521
  await promises.writeFile(tmp, content, "utf8");
2039
522
  await promises.rename(tmp, abs);
2040
523
  const st = await promises.stat(abs);
524
+ const beforeClamped = existed ? clampHistoryValue(before) : void 0;
525
+ const afterClamped = clampHistoryValue(content);
526
+ await appendHistory(agentDir, {
527
+ path: relPath,
528
+ event: existed ? "update" : "add",
529
+ ...beforeClamped ? { before: beforeClamped.value } : {},
530
+ after: afterClamped.value,
531
+ at: st.mtime.toISOString(),
532
+ scope: "agent",
533
+ source,
534
+ ...afterClamped.truncated || beforeClamped?.truncated ? { truncated: true } : {}
535
+ });
2041
536
  return {
2042
537
  size: st.size,
2043
538
  mtime: st.mtime.toISOString()
@@ -2056,7 +551,7 @@ async function listMemory(agentDir) {
2056
551
  }
2057
552
  const files = [];
2058
553
  for (const d of dirents) {
2059
- if (!d.isFile() || d.name.endsWith(".tmp")) continue;
554
+ if (!d.isFile() || d.name.endsWith(".tmp") || d.name.startsWith(".")) continue;
2060
555
  try {
2061
556
  const st = await promises.stat(join(dir, d.name));
2062
557
  files.push({
@@ -2263,8 +758,8 @@ var ManagedMemoryProvider = class {
2263
758
  content: await readMemoryFile(this.dirFor(scope.agentId), path)
2264
759
  };
2265
760
  }
2266
- async write(scope, path, content, ifMatch) {
2267
- const { size, mtime } = await writeMemoryFile(this.dirFor(scope.agentId), path, content, ifMatch);
761
+ async write(scope, path, content, ifMatch, source) {
762
+ const { size, mtime } = await writeMemoryFile(this.dirFor(scope.agentId), path, content, ifMatch, source);
2268
763
  return {
2269
764
  ok: true,
2270
765
  path,
@@ -2315,7 +810,7 @@ var NativeMemoryProvider = class {
2315
810
  async read(scope, path) {
2316
811
  return nativeMemoryRead(this.dirFor(scope.agentId), this.runtimeFor(scope.agentId), path);
2317
812
  }
2318
- async write(scope, path, content, ifMatch) {
813
+ async write(scope, path, content, ifMatch, _source) {
2319
814
  return nativeMemoryWrite(this.dirFor(scope.agentId), this.runtimeFor(scope.agentId), path, content, ifMatch);
2320
815
  }
2321
816
  };
@@ -2370,8 +865,8 @@ var DispatchingMemoryProvider = class {
2370
865
  read(scope, path) {
2371
866
  return this.forAgent(scope.agentId).read(scope, path);
2372
867
  }
2373
- write(scope, path, content, ifMatch) {
2374
- return this.forAgent(scope.agentId).write(scope, path, content, ifMatch);
868
+ write(scope, path, content, ifMatch, source) {
869
+ return this.forAgent(scope.agentId).write(scope, path, content, ifMatch, source);
2375
870
  }
2376
871
  /** The memory tools to inject for a specific agent (the daemon calls this in
2377
872
  * `mcpServersFor` — native contributes none). Scope-carrying, unlike the
@@ -11504,6 +9999,6 @@ var AcpHost = class {
11504
9999
  }
11505
10000
  };
11506
10001
  //#endregion
11507
- export { toolsForIntegrations as A, decodeRelayDaemonFrame as C, MAX_FRAME_BYTES as D, encode as E, cachedRuntimeVersions as M, resolveRuntimes as N, RESERVED_MCP_SERVER_NAME as O, buildRelayDaemonFrame as S, decodeEnvelope as T, writeMemoryFile as _, initGitInjection as a, RELAY_DAEMON_SUBPROTOCOL as b, simpleGit as c, memoryProviderFor as d, MemoryConflictError as f, resolveInMemoryDir as g, listMemory as h, prepareWorkspace as i, cachedRuntimeNames as j, MEMORY_TOOL_NAMES as k, createMemoryProvider as l, MemoryTooLargeError as m, installedRuntimes as n, probeGitVersion as o, MemoryPathError as p, prefetchWorkspace as r, sessionGitEnv as s, AcpHost as t, memoryKindOf as u, isFrame as v, buildEnvelope as w, RELAY_DAEMON_WS_PATH as x, normalizeGitUrl as y };
10002
+ export { resolveRuntimes as S, writeMemoryFile as _, initGitInjection as a, cachedRuntimeNames as b, simpleGit as c, memoryProviderFor as d, MemoryConflictError as f, resolveInMemoryDir as g, listMemory as h, prepareWorkspace as i, createMemoryProvider as l, MemoryTooLargeError as m, installedRuntimes as n, probeGitVersion as o, MemoryPathError as p, prefetchWorkspace as r, sessionGitEnv as s, AcpHost as t, memoryKindOf as u, MEMORY_TOOL_NAMES as v, cachedRuntimeVersions as x, toolsForIntegrations as y };
11508
10003
 
11509
- //# sourceMappingURL=acp-host-iKdfasdN.js.map
10004
+ //# sourceMappingURL=acp-host-CUct-2CR.js.map