@desplega.ai/agent-swarm 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-v85a2bqy.js} +1 -1
  2. package/dist/{actions-w4pnb8vz.js → actions-zgcxfspj.js} +15 -15
  3. package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-jw9xfz42.js} +2 -2
  4. package/dist/{app-zneem7g9.js → app-4e1s4675.js} +3 -3
  5. package/dist/{assistant-wbzd2rnr.js → assistant-v238n9dk.js} +16 -16
  6. package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-p666cw5n.js} +1 -1
  7. package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-w6pm64j4.js} +1 -1
  8. package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-jcyjagn3.js} +2 -2
  9. package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-gbedmamp.js} +4 -4
  10. package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-q7wspzt8.js} +2 -2
  11. package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-wza784fb.js} +1 -1
  12. package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-yf400ptr.js} +2 -2
  13. package/dist/{cli-p46xdr1m.js → cli-0rh20bp3.js} +1 -1
  14. package/dist/{cli-e5y285rg.js → cli-1v8w782f.js} +1 -1
  15. package/dist/{cli-8c1m9mhy.js → cli-30m5q8mc.js} +5 -5
  16. package/dist/{cli-xnp2vkx7.js → cli-4hmaheb6.js} +6 -6
  17. package/dist/{cli-xhgtay7w.js → cli-4s1kqrqg.js} +1 -1
  18. package/dist/{cli-2jm992rm.js → cli-5d1m1qca.js} +2 -2
  19. package/dist/{cli-mvsfbbgq.js → cli-65q70v2v.js} +3 -3
  20. package/dist/{cli-th8krqbf.js → cli-6p7ddg72.js} +2 -2
  21. package/dist/{cli-6q7pyd3k.js → cli-6vcyw6v7.js} +1 -1
  22. package/dist/{cli-bddxamg7.js → cli-8qsx817b.js} +23 -22
  23. package/dist/{cli-ztcxv5nv.js → cli-9e9kjqsj.js} +1 -1
  24. package/dist/{cli-jrk8mj0t.js → cli-a17tzmn0.js} +1 -1
  25. package/dist/{cli-raejy0mp.js → cli-d43xqzbe.js} +1 -1
  26. package/dist/{cli-tba8fxf9.js → cli-dxwh4j32.js} +11 -11
  27. package/dist/{cli-c9vrwcpb.js → cli-ea7chd26.js} +19 -19
  28. package/dist/{cli-vmbakbq0.js → cli-fhvqt2fp.js} +1 -1
  29. package/dist/{cli-z3dk189k.js → cli-fp4xbw07.js} +6 -6
  30. package/dist/{cli-xx13gqf3.js → cli-g6p7c2my.js} +1 -1
  31. package/dist/{cli-c0dpzwnx.js → cli-hn3pftj2.js} +2 -2
  32. package/dist/{cli-h23mr2s6.js → cli-hz64bcnj.js} +2 -2
  33. package/dist/{cli-dw0njd9q.js → cli-jhmjwqcg.js} +5 -5
  34. package/dist/{cli-0k9rcwfy.js → cli-ka01m5rb.js} +28 -12
  35. package/dist/{cli-jhhb2v9n.js → cli-kqypty34.js} +2 -2
  36. package/dist/{cli-hwc4a9xm.js → cli-q1812jbw.js} +4 -4
  37. package/dist/{cli-8w65vwee.js → cli-qrwp40st.js} +1 -1
  38. package/dist/{cli-4c7k75rq.js → cli-re4wkbzx.js} +54 -41
  39. package/dist/{cli-z1hh99j0.js → cli-tr24eeny.js} +2 -2
  40. package/dist/{cli-q6gw45wj.js → cli-wtg6edvd.js} +1 -1
  41. package/dist/{cli-mnry16kh.js → cli-xgn4rwyv.js} +4 -4
  42. package/dist/{cli-5ks6x36q.js → cli-y3dvz6ge.js} +3 -3
  43. package/dist/{cli-ksrphveb.js → cli-yv0vgscc.js} +1 -0
  44. package/dist/{cli-ak4c12mh.js → cli-yv3n83x8.js} +1 -1
  45. package/dist/{cli-9ac0mkg5.js → cli-zes4tsbb.js} +1 -1
  46. package/dist/cli.js +14 -14
  47. package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-nhjj2veb.js} +1 -1
  48. package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-6hggrjbx.js} +1 -1
  49. package/dist/{commands-ktnqwrv4.js → commands-mrw1ph2w.js} +2 -2
  50. package/dist/{controller-nex2he8v.js → controller-pt1zam5w.js} +2 -2
  51. package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-ymw5541d.js} +16 -16
  52. package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-cf4zh5x0.js} +1 -1
  53. package/dist/{db-19tw9r5s.js → db-hhpy6m4x.js} +2 -2
  54. package/dist/{db-jvr86v7f.js → db-hy80219q.js} +1 -1
  55. package/dist/{dispatcher-d3mnsg2y.js → dispatcher-6pdjtkr1.js} +14 -14
  56. package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-fbx06re2.js} +1 -1
  57. package/dist/{external-channel-44cwer32.js → external-channel-j6vb6ya1.js} +2 -2
  58. package/dist/{grok-adapter-ysp907f6.js → grok-adapter-1193sprp.js} +2 -2
  59. package/dist/{handlers-t5pbabwf.js → handlers-tb3xgjnm.js} +17 -17
  60. package/dist/{heartbeat-2etacsqq.js → heartbeat-h1w1ktce.js} +17 -17
  61. package/dist/{hook-3kvmax6d.js → hook-ejja4bpe.js} +1 -1
  62. package/dist/{hook-mzxxs5n2.js → hook-skrbja18.js} +1 -1
  63. package/dist/{http-fn978m0g.js → http-mzh41mfj.js} +108 -52
  64. package/dist/{index-039x2sag.js → index-6rmnn89d.js} +40 -12
  65. package/dist/{index-w2wr89xd.js → index-epr2gnbw.js} +1 -1
  66. package/dist/{index-qyjbb7br.js → index-nyfcdpky.js} +17 -17
  67. package/dist/{index-e4tsa1h7.js → index-vjn3wbzf.js} +14 -14
  68. package/dist/{index-1r8hw4vd.js → index-xmmkyx82.js} +14 -14
  69. package/dist/{keepalive-d4nzs76x.js → keepalive-4djvbm2j.js} +3 -3
  70. package/dist/{lead-8h5n9k5f.js → lead-61kqp604.js} +23 -23
  71. package/dist/{maintenance-ea6jfw42.js → maintenance-f3xhnjsm.js} +1 -1
  72. package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-fpd6x6rw.js} +2 -2
  73. package/dist/{onboard-pvk7hdb2.js → onboard-196tjgyn.js} +2 -2
  74. package/dist/{otel-impl-qb7nhgdp.js → otel-impl-g8fh4c21.js} +1 -1
  75. package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-sdcf5yaz.js} +3 -3
  76. package/dist/{rbac-roles-9648nh00.js → rbac-roles-2x4g1wb5.js} +2 -2
  77. package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-bsp5657v.js} +1 -1
  78. package/dist/{render-v2-xkze1wef.js → render-v2-0pyb39bb.js} +3 -3
  79. package/dist/{resume-tq0bbvt5.js → resume-a86tkj6r.js} +5 -5
  80. package/dist/{setup-rnrazxvd.js → setup-ypbzcapd.js} +1 -1
  81. package/dist/{work-objects-e75q2rqs.js → work-objects-yjp8de12.js} +17 -17
  82. package/dist/{worker-4ww4hjef.js → worker-4ptnn1ge.js} +23 -23
  83. package/openapi.json +168 -6
  84. package/package.json +2 -2
  85. package/src/be/asset-key-audit.ts +4 -13
  86. package/src/be/db.ts +57 -33
  87. package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
  88. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
  89. package/src/heartbeat/heartbeat.ts +25 -7
  90. package/src/http/assets.ts +26 -4
  91. package/src/http/inbox-state.ts +58 -8
  92. package/src/kv-reserved-namespaces.ts +1 -0
  93. package/src/providers/amp-adapter.ts +1 -1
  94. package/src/tests/asset-key-api.test.ts +128 -0
  95. package/src/tests/asset-key-invariants.test.ts +179 -3
  96. package/src/tests/asset-key-migration.test.ts +35 -0
  97. package/src/tests/docs-index-footprint.test.js +2 -2
  98. package/src/tests/heartbeat-checklist.test.ts +88 -0
  99. package/src/tests/heartbeat-jev-gate.test.ts +394 -10
  100. package/src/tests/inbox-state.test.ts +167 -0
  101. package/src/tests/kv-tool.test.ts +15 -0
  102. package/src/tests/script-connections.test.ts +2 -0
  103. package/src/types.ts +2 -0
  104. package/templates/skills/code-quality/content.md +2 -2
  105. package/templates/skills/pages/content.md +4 -0
  106. package/templates/skills/swarm-scripts/SKILL.md +6 -0
  107. package/templates/skills/swarm-scripts/content.md +6 -0
@@ -1,21 +1,21 @@
1
1
  import"./cli-qz93y3x5.js";
2
2
  import {
3
3
  getEnabledCapabilities
4
- } from "./cli-bddxamg7.js";
4
+ } from "./cli-8qsx817b.js";
5
5
  import"./cli-v1sjhn52.js";
6
- import"./cli-4c7k75rq.js";
7
- import"./cli-6q7pyd3k.js";
8
- import"./cli-mnry16kh.js";
9
- import"./cli-8w65vwee.js";
10
- import"./cli-jhhb2v9n.js";
11
- import"./cli-vmbakbq0.js";
12
- import"./cli-dw0njd9q.js";
13
- import"./cli-mvsfbbgq.js";
14
- import"./cli-xx13gqf3.js";
6
+ import"./cli-re4wkbzx.js";
7
+ import"./cli-6vcyw6v7.js";
8
+ import"./cli-xgn4rwyv.js";
9
+ import"./cli-qrwp40st.js";
10
+ import"./cli-kqypty34.js";
11
+ import"./cli-fhvqt2fp.js";
12
+ import"./cli-jhmjwqcg.js";
13
+ import"./cli-65q70v2v.js";
14
+ import"./cli-g6p7c2my.js";
15
15
  import"./cli-3ggwdtb0.js";
16
- import"./cli-tba8fxf9.js";
17
- import"./cli-ztcxv5nv.js";
18
- import"./cli-hwc4a9xm.js";
16
+ import"./cli-dxwh4j32.js";
17
+ import"./cli-9e9kjqsj.js";
18
+ import"./cli-q1812jbw.js";
19
19
  import"./cli-8cvvytad.js";
20
20
  import"./cli-kvzgak7c.js";
21
21
  import"./cli-445c1vp7.js";
@@ -23,7 +23,7 @@ import"./cli-vjfpshn3.js";
23
23
  import"./cli-ptkpaqfy.js";
24
24
  import"./cli-7a91w22y.js";
25
25
  import"./cli-qje9k2nh.js";
26
- import"./cli-h23mr2s6.js";
26
+ import"./cli-hz64bcnj.js";
27
27
  import"./cli-3h26v7yj.js";
28
28
  import"./cli-rdr166ht.js";
29
29
  import"./cli-665j3feb.js";
@@ -31,31 +31,31 @@ import"./cli-p22zdvnt.js";
31
31
  import"./cli-77cd6gy8.js";
32
32
  import {
33
33
  runAgent
34
- } from "./cli-c9vrwcpb.js";
34
+ } from "./cli-ea7chd26.js";
35
35
  import"./cli-jhp7w1fy.js";
36
36
  import"./cli-gx4rm0qx.js";
37
37
  import"./cli-a49vpbf3.js";
38
38
  import"./cli-jc1g9ndz.js";
39
- import"./cli-th8krqbf.js";
39
+ import"./cli-6p7ddg72.js";
40
40
  import"./cli-k28p6xh7.js";
41
41
  import"./cli-etvn5ej2.js";
42
42
  import"./cli-c5sbmw1v.js";
43
43
  import"./cli-0726awxw.js";
44
- import"./cli-e5y285rg.js";
45
- import"./cli-c0dpzwnx.js";
44
+ import"./cli-1v8w782f.js";
45
+ import"./cli-hn3pftj2.js";
46
46
  import"./cli-4bd1rck6.js";
47
47
  import"./cli-vbhgkw9h.js";
48
48
  import"./cli-er83cjbn.js";
49
49
  import"./cli-j0v75y13.js";
50
50
  import"./cli-kr5tjjtz.js";
51
- import"./cli-z1hh99j0.js";
52
- import"./cli-xhgtay7w.js";
51
+ import"./cli-tr24eeny.js";
52
+ import"./cli-4s1kqrqg.js";
53
53
  import"./cli-zv4vy2b0.js";
54
- import"./cli-9ac0mkg5.js";
55
- import"./cli-jrk8mj0t.js";
54
+ import"./cli-zes4tsbb.js";
55
+ import"./cli-a17tzmn0.js";
56
56
  import"./cli-9td5ss65.js";
57
57
  import"./cli-rc3r0bnh.js";
58
- import"./cli-p46xdr1m.js";
58
+ import"./cli-0rh20bp3.js";
59
59
  import"./cli-4zsenjhy.js";
60
60
  import"./cli-4g629sgr.js";
61
61
  import"./cli-13p25grh.js";
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "2.2.0",
5
+ "version": "2.3.0",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
@@ -2295,6 +2295,9 @@
2295
2295
  "readAt": {
2296
2296
  "type": "string"
2297
2297
  },
2298
+ "lastReadAt": {
2299
+ "type": "string"
2300
+ },
2298
2301
  "createdAt": {
2299
2302
  "type": "string"
2300
2303
  },
@@ -14653,6 +14656,12 @@
14653
14656
  "minLength": 1,
14654
14657
  "maxLength": 255,
14655
14658
  "description": "Non-unique asset directory namespace (for example shared/ or personal/<user-id>/drafts/). Runtime write boundaries normalize and validate the canonical form."
14659
+ },
14660
+ "movedTaskIds": {
14661
+ "type": "array",
14662
+ "items": {
14663
+ "type": "string"
14664
+ }
14656
14665
  }
14657
14666
  },
14658
14667
  "required": [
@@ -14777,6 +14786,12 @@
14777
14786
  "minLength": 1,
14778
14787
  "maxLength": 255,
14779
14788
  "description": "Non-unique asset directory namespace (for example shared/ or personal/<user-id>/drafts/). Runtime write boundaries normalize and validate the canonical form."
14789
+ },
14790
+ "movedTaskIds": {
14791
+ "type": "array",
14792
+ "items": {
14793
+ "type": "string"
14794
+ }
14780
14795
  }
14781
14796
  },
14782
14797
  "required": [
@@ -14882,6 +14897,10 @@
14882
14897
  "minLength": 1,
14883
14898
  "maxLength": 255,
14884
14899
  "description": "Non-unique asset directory namespace (for example shared/ or personal/<user-id>/drafts/). Runtime write boundaries normalize and validate the canonical form."
14900
+ },
14901
+ "cascade": {
14902
+ "type": "boolean",
14903
+ "description": "For tasks only, also move every descendant task recursively through parentTaskId."
14885
14904
  }
14886
14905
  },
14887
14906
  "required": [
@@ -14919,6 +14938,12 @@
14919
14938
  "minLength": 1,
14920
14939
  "maxLength": 255,
14921
14940
  "description": "Non-unique asset directory namespace (for example shared/ or personal/<user-id>/drafts/). Runtime write boundaries normalize and validate the canonical form."
14941
+ },
14942
+ "movedTaskIds": {
14943
+ "type": "array",
14944
+ "items": {
14945
+ "type": "string"
14946
+ }
14922
14947
  }
14923
14948
  },
14924
14949
  "required": [
@@ -20766,9 +20791,6 @@
20766
20791
  "schema": {
20767
20792
  "type": "object",
20768
20793
  "properties": {
20769
- "userId": {
20770
- "type": "string"
20771
- },
20772
20794
  "itemType": {
20773
20795
  "type": "string",
20774
20796
  "enum": [
@@ -20800,13 +20822,24 @@
20800
20822
  "readAt": {
20801
20823
  "type": "string",
20802
20824
  "format": "date-time"
20825
+ },
20826
+ "lastReadAt": {
20827
+ "type": [
20828
+ "string",
20829
+ "null"
20830
+ ],
20831
+ "format": "date-time",
20832
+ "description": "Omit to preserve the read cursor, send null to clear it, or send a timestamp to set it, including an earlier time."
20833
+ },
20834
+ "userId": {
20835
+ "type": "string"
20803
20836
  }
20804
20837
  },
20805
20838
  "required": [
20806
- "userId",
20807
20839
  "itemType",
20808
20840
  "itemId",
20809
- "status"
20841
+ "status",
20842
+ "userId"
20810
20843
  ]
20811
20844
  }
20812
20845
  }
@@ -20854,6 +20887,135 @@
20854
20887
  }
20855
20888
  }
20856
20889
  },
20890
+ "/api/inbox-state/bulk": {
20891
+ "patch": {
20892
+ "summary": "Upsert inbox-item state in one transaction",
20893
+ "tags": [
20894
+ "Inbox State"
20895
+ ],
20896
+ "security": [
20897
+ {
20898
+ "bearerAuth": []
20899
+ }
20900
+ ],
20901
+ "requestBody": {
20902
+ "content": {
20903
+ "application/json": {
20904
+ "schema": {
20905
+ "type": "object",
20906
+ "properties": {
20907
+ "userId": {
20908
+ "type": "string"
20909
+ },
20910
+ "items": {
20911
+ "type": "array",
20912
+ "items": {
20913
+ "type": "object",
20914
+ "properties": {
20915
+ "itemType": {
20916
+ "type": "string",
20917
+ "enum": [
20918
+ "approval",
20919
+ "credential_missing",
20920
+ "broken_task",
20921
+ "to_read",
20922
+ "to_start_template",
20923
+ "notification"
20924
+ ]
20925
+ },
20926
+ "itemId": {
20927
+ "type": "string",
20928
+ "minLength": 1
20929
+ },
20930
+ "status": {
20931
+ "type": "string",
20932
+ "enum": [
20933
+ "open",
20934
+ "snoozed",
20935
+ "dismissed",
20936
+ "done"
20937
+ ]
20938
+ },
20939
+ "snoozeUntil": {
20940
+ "type": "string",
20941
+ "format": "date-time"
20942
+ },
20943
+ "readAt": {
20944
+ "type": "string",
20945
+ "format": "date-time"
20946
+ },
20947
+ "lastReadAt": {
20948
+ "type": [
20949
+ "string",
20950
+ "null"
20951
+ ],
20952
+ "format": "date-time",
20953
+ "description": "Omit to preserve the read cursor, send null to clear it, or send a timestamp to set it, including an earlier time."
20954
+ }
20955
+ },
20956
+ "required": [
20957
+ "itemType",
20958
+ "itemId",
20959
+ "status"
20960
+ ]
20961
+ },
20962
+ "minItems": 1,
20963
+ "maxItems": 200
20964
+ }
20965
+ },
20966
+ "required": [
20967
+ "userId",
20968
+ "items"
20969
+ ]
20970
+ }
20971
+ }
20972
+ }
20973
+ },
20974
+ "responses": {
20975
+ "200": {
20976
+ "description": "Upserted inbox state rows in request order",
20977
+ "content": {
20978
+ "application/json": {
20979
+ "schema": {
20980
+ "type": "object",
20981
+ "properties": {
20982
+ "items": {
20983
+ "type": "array",
20984
+ "items": {
20985
+ "$ref": "#/components/schemas/InboxItemState"
20986
+ }
20987
+ }
20988
+ },
20989
+ "required": [
20990
+ "items"
20991
+ ]
20992
+ }
20993
+ }
20994
+ }
20995
+ },
20996
+ "400": {
20997
+ "description": "Validation error",
20998
+ "content": {
20999
+ "application/json": {
21000
+ "schema": {
21001
+ "$ref": "#/components/schemas/ErrorResponse"
21002
+ }
21003
+ }
21004
+ }
21005
+ },
21006
+ "401": {
21007
+ "description": "Unauthorized",
21008
+ "content": {
21009
+ "application/json": {
21010
+ "schema": {
21011
+ "$ref": "#/components/schemas/ErrorResponse"
21012
+ }
21013
+ }
21014
+ }
21015
+ }
21016
+ }
21017
+ }
21018
+ },
20857
21019
  "/api/integrations/claude-managed/test": {
20858
21020
  "post": {
20859
21021
  "summary": "Test the claude-managed integration: resolves ANTHROPIC_API_KEY + MANAGED_AGENT_ID from swarm_config and calls beta.agents.retrieve.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
5
5
  "keywords": [
6
6
  "agent-swarm.dev",
@@ -209,7 +209,7 @@
209
209
  "@chat-adapter/teams": "4.41.1",
210
210
  "@chat-adapter/telegram": "4.41.1",
211
211
  "@cursor/sdk": "1.0.38",
212
- "@desplega.ai/agent-fs-just-bash": "0.17.0",
212
+ "@desplega.ai/agent-fs-just-bash": "0.18.4",
213
213
  "@desplega.ai/business-use": "^0.4.2",
214
214
  "@earendil-works/pi-agent-core": "^1.3.0",
215
215
  "@earendil-works/pi-ai": "^1.3.0",
@@ -90,9 +90,7 @@ function auditKeyRow(
90
90
 
91
91
  type AttachmentMappingAuditRow = {
92
92
  attachment_id: string;
93
- task_key: string;
94
93
  mapping_id: string | null;
95
- mapping_key: string | null;
96
94
  };
97
95
 
98
96
  type MappingSourceAuditRow = {
@@ -118,9 +116,7 @@ export function auditAssetKeys(db: Database): AssetKeyAuditResult {
118
116
  .prepare<AttachmentMappingAuditRow, []>(
119
117
  `SELECT
120
118
  a.id AS attachment_id,
121
- t."key" AS task_key,
122
- m.id AS mapping_id,
123
- m."key" AS mapping_key
119
+ m.id AS mapping_id
124
120
  FROM task_attachments a
125
121
  JOIN agent_tasks t ON t.id = a.task_id
126
122
  LEFT JOIN asset_key_mappings m
@@ -132,6 +128,9 @@ export function auditAssetKeys(db: Database): AssetKeyAuditResult {
132
128
  AND COALESCE(NULLIF(a.provider_key, ''), a.path) IS NOT NULL`,
133
129
  )
134
130
  .all();
131
+ // One file has one mapping, which follows its source attachment (checked below). Other
132
+ // tasks may attach the same file from any namespace, and a mapping detached from its
133
+ // deleted source keeps its own namespace, so neither is drift.
135
134
  for (const row of attachmentMappings) {
136
135
  if (!row.mapping_id) {
137
136
  issues.push({
@@ -141,14 +140,6 @@ export function auditAssetKeys(db: Database): AssetKeyAuditResult {
141
140
  entityId: row.attachment_id,
142
141
  message: "Agent-fs attachment has no logical namespace mapping.",
143
142
  });
144
- } else if (row.mapping_key !== row.task_key) {
145
- issues.push({
146
- severity: "warning",
147
- code: "provider-mapping-drift",
148
- entityType: "file",
149
- entityId: row.mapping_id,
150
- message: "Provider mapping namespace differs from its parent task namespace.",
151
- });
152
143
  }
153
144
  }
154
145
 
package/src/be/db.ts CHANGED
@@ -1731,7 +1731,9 @@ export async function moveAssetKey(input: {
1731
1731
  id: string;
1732
1732
  key: string;
1733
1733
  changedBy?: string;
1734
- }): Promise<boolean> {
1734
+ /** Task moves only: descendant task ids the caller already authorized to move with the task. */
1735
+ descendantTaskIds?: readonly string[];
1736
+ }): Promise<false | { movedTaskIds?: string[] }> {
1735
1737
  const key = normalizeAssetKey(input.key);
1736
1738
  // auditAssetKeys stays sync (shared with the boot audit); run it inside a
1737
1739
  // client transaction so this request-path read cannot observe another
@@ -1746,7 +1748,7 @@ export async function moveAssetKey(input: {
1746
1748
  }
1747
1749
  const previousKey = await currentAssetKey(input.entityType, input.id);
1748
1750
  if (!previousKey) return false;
1749
- if (previousKey === key) return true;
1751
+ if (previousKey === key && input.entityType !== "task") return {};
1750
1752
  if (input.entityType === "file") {
1751
1753
  const mapping = await getAssetKeyMapping(input.id);
1752
1754
  if (mapping?.sourceEntityType === "task-attachment") {
@@ -1757,48 +1759,64 @@ export async function moveAssetKey(input: {
1757
1759
  }
1758
1760
  const now = new Date().toISOString();
1759
1761
 
1760
- await getDbClient().transaction(async (tx) => {
1762
+ return getDbClient().transaction(async (tx) => {
1761
1763
  switch (input.entityType) {
1762
1764
  case "task": {
1763
- const mappedFiles = await tx.query<{ id: string; key: string }>(
1764
- `SELECT DISTINCT m.id, m."key" AS key
1765
+ const root = await getTaskById(input.id);
1766
+ if (!root) return false;
1767
+ const tasks = [root];
1768
+ for (const id of input.descendantTaskIds ?? []) {
1769
+ const task = id === root.id ? null : await getTaskById(id);
1770
+ if (task) tasks.push(task);
1771
+ }
1772
+ const movedTaskIds: string[] = [];
1773
+ for (const task of tasks) {
1774
+ if (task.key === key) continue;
1775
+ // A file shared with another task keeps the namespace of the task whose attachment
1776
+ // sources its mapping. Mappings with no attachment source follow any attaching task.
1777
+ const mappedFiles = await tx.query<{ id: string; key: string }>(
1778
+ `SELECT DISTINCT m.id, m."key" AS key
1765
1779
  FROM asset_key_mappings m
1766
1780
  JOIN task_attachments a
1767
1781
  ON m.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
1768
1782
  AND m.provider_org_id = COALESCE(a.agent_fs_org_id, '')
1769
1783
  AND m.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
1770
1784
  AND m.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
1771
- WHERE a.task_id = ?`,
1772
- [input.id],
1773
- );
1774
- await tx.run(
1775
- 'UPDATE agent_tasks SET "key" = ?, lastUpdatedAt = ?, updated_by = ? WHERE id = ?',
1776
- [key, now, input.changedBy ?? null, input.id],
1777
- );
1778
- await tx.run(
1779
- `UPDATE asset_key_mappings
1780
- SET "key" = ?, updated_at = ?, updated_by = ?
1781
- WHERE EXISTS (
1782
- SELECT 1 FROM task_attachments a
1783
1785
  WHERE a.task_id = ?
1784
- AND asset_key_mappings.provider_id = COALESCE(NULLIF(a.provider_id, ''), 'agent-fs')
1785
- AND asset_key_mappings.provider_org_id = COALESCE(a.agent_fs_org_id, '')
1786
- AND asset_key_mappings.provider_drive_id = COALESCE(a.agent_fs_drive_id, '')
1787
- AND asset_key_mappings.provider_key = COALESCE(NULLIF(a.provider_key, ''), a.path)
1788
- )`,
1789
- [key, now, input.changedBy ?? null, input.id],
1790
- );
1791
- for (const mapping of mappedFiles) {
1792
- if (mapping.key === key) continue;
1786
+ AND (m.source_entity_type IS NOT 'task-attachment'
1787
+ OR m.source_entity_id IN (SELECT id FROM task_attachments WHERE task_id = ?))`,
1788
+ [task.id, task.id],
1789
+ );
1790
+ // The namespace is metadata, and lastUpdatedAt tracks task activity, as in updateTaskTitle.
1791
+ await tx.run('UPDATE agent_tasks SET "key" = ?, updated_by = ? WHERE id = ?', [
1792
+ key,
1793
+ input.changedBy ?? null,
1794
+ task.id,
1795
+ ]);
1796
+ for (const mapping of mappedFiles) {
1797
+ if (mapping.key === key) continue;
1798
+ await tx.run(
1799
+ 'UPDATE asset_key_mappings SET "key" = ?, updated_at = ?, updated_by = ? WHERE id = ?',
1800
+ [key, now, input.changedBy ?? null, mapping.id],
1801
+ );
1802
+ await insertAssetKeyHistory({
1803
+ entityType: "file",
1804
+ entityId: mapping.id,
1805
+ previousKey: mapping.key,
1806
+ newKey: key,
1807
+ changedBy: input.changedBy,
1808
+ });
1809
+ }
1793
1810
  await insertAssetKeyHistory({
1794
- entityType: "file",
1795
- entityId: mapping.id,
1796
- previousKey: mapping.key,
1811
+ entityType: "task",
1812
+ entityId: task.id,
1813
+ previousKey: task.key,
1797
1814
  newKey: key,
1798
1815
  changedBy: input.changedBy,
1799
1816
  });
1817
+ movedTaskIds.push(task.id);
1800
1818
  }
1801
- break;
1819
+ return { movedTaskIds };
1802
1820
  }
1803
1821
  case "workflow":
1804
1822
  await tx.run(
@@ -1849,8 +1867,8 @@ export async function moveAssetKey(input: {
1849
1867
  newKey: key,
1850
1868
  changedBy: input.changedBy,
1851
1869
  });
1870
+ return {};
1852
1871
  });
1853
- return true;
1854
1872
  }
1855
1873
 
1856
1874
  export async function deleteTaskAttachment(id: string): Promise<boolean> {
@@ -12513,6 +12531,7 @@ interface InboxItemStateRow {
12513
12531
  dismissedAt: string | null;
12514
12532
  doneAt: string | null;
12515
12533
  readAt: string | null;
12534
+ lastReadAt: string | null;
12516
12535
  createdAt: string;
12517
12536
  lastUpdatedAt: string;
12518
12537
  }
@@ -12528,6 +12547,7 @@ function rowToInboxItemState(row: InboxItemStateRow): InboxItemState {
12528
12547
  dismissedAt: row.dismissedAt ?? undefined,
12529
12548
  doneAt: row.doneAt ?? undefined,
12530
12549
  readAt: row.readAt ?? undefined,
12550
+ lastReadAt: row.lastReadAt ?? undefined,
12531
12551
  createdAt: row.createdAt,
12532
12552
  lastUpdatedAt: row.lastUpdatedAt,
12533
12553
  };
@@ -12568,6 +12588,7 @@ export async function upsertInboxState(opts: {
12568
12588
  doneAt?: string;
12569
12589
  /** First-viewed timestamp. Sticky: once set, later calls never clear or move it. */
12570
12590
  readAt?: string;
12591
+ lastReadAt?: string | null;
12571
12592
  }): Promise<InboxItemState> {
12572
12593
  const now = new Date().toISOString();
12573
12594
  // Auto-derive timestamps from status when not explicitly provided.
@@ -12579,14 +12600,15 @@ export async function upsertInboxState(opts: {
12579
12600
  // SQLite upsert via UNIQUE(userId, itemType, itemId). readAt is sticky: the
12580
12601
  // first non-null value written wins on every subsequent conflict.
12581
12602
  const row = await getDbClient().get<InboxItemStateRow>(
12582
- `INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, readAt, createdAt, lastUpdatedAt)
12583
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12603
+ `INSERT INTO inbox_item_state (userId, itemType, itemId, status, snoozeUntil, dismissedAt, doneAt, readAt, lastReadAt, createdAt, lastUpdatedAt)
12604
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
12584
12605
  ON CONFLICT(userId, itemType, itemId) DO UPDATE SET
12585
12606
  status = excluded.status,
12586
12607
  snoozeUntil = excluded.snoozeUntil,
12587
12608
  dismissedAt = excluded.dismissedAt,
12588
12609
  doneAt = excluded.doneAt,
12589
12610
  readAt = COALESCE(inbox_item_state.readAt, excluded.readAt),
12611
+ lastReadAt = CASE WHEN ? THEN excluded.lastReadAt ELSE inbox_item_state.lastReadAt END,
12590
12612
  lastUpdatedAt = excluded.lastUpdatedAt
12591
12613
  RETURNING *`,
12592
12614
  [
@@ -12598,8 +12620,10 @@ export async function upsertInboxState(opts: {
12598
12620
  dismissedAt,
12599
12621
  doneAt,
12600
12622
  readAt,
12623
+ opts.lastReadAt ?? null,
12601
12624
  now,
12602
12625
  now,
12626
+ opts.lastReadAt !== undefined ? 1 : 0,
12603
12627
  ],
12604
12628
  );
12605
12629
  if (!row) throw new Error("Failed to upsert inbox state");
@@ -0,0 +1 @@
1
+ ALTER TABLE inbox_item_state ADD COLUMN lastReadAt TEXT;
@@ -138,8 +138,7 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
138
138
  // db_query_concurrency_cap on a busy swarm.
139
139
  const lead = await q("SELECT heartbeatMd FROM agents WHERE id = ?", [leadId]);
140
140
  const prev = await q(
141
- `SELECT id, substr(output, 1, 400) AS output,
142
- substr(task, 1, instr(task, '## Your Standing Orders')) AS status
141
+ `SELECT id, substr(output, 1, 400) AS output
143
142
  FROM agent_tasks
144
143
  WHERE agentId = ? AND taskType = 'heartbeat-checklist' AND status = 'completed'
145
144
  ORDER BY createdAt DESC LIMIT 1`,
@@ -163,9 +162,28 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
163
162
  "SELECT status, COUNT(*) AS n FROM agents WHERE COALESCE(role, '') != 'extension' GROUP BY status",
164
163
  );
165
164
 
165
+ // Only the producer's receipt proves which full IDs the checklist actually showed.
166
+ // Old checklists and invalid receipts conservatively treat all current failures as new.
167
+ let reportedIds: unknown = [];
168
+ if (prev[0]?.id) {
169
+ try {
170
+ const receipt = await ctx.swarm.kv_getOrNull({
171
+ namespace: "heartbeat-checklist:reported-failures",
172
+ key: prev[0].id,
173
+ });
174
+ reportedIds = typeof receipt?.value === "string" ? JSON.parse(receipt.value) : receipt?.value;
175
+ } catch {
176
+ reportedIds = [];
177
+ }
178
+ }
179
+ const prevIds = new Set(
180
+ Array.isArray(reportedIds) && reportedIds.every((id) => typeof id === "string") ? reportedIds : [],
181
+ );
166
182
  const fails = parseFailures(failed.map(renderFailureLine).join("\n"));
167
- const prevIds = new Set(parseFailures(String(prev[0]?.status ?? "")).map((f) => f.id));
168
- const newFails = fails.filter((f) => !prevIds.has(f.id));
183
+ // Compare full IDs before rendering the replay's eight-character display IDs.
184
+ const newFails = parseFailures(
185
+ failed.filter((f) => !prevIds.has(f.id)).map(renderFailureLine).join("\n"),
186
+ );
169
187
 
170
188
  const { stamp, zone } = localStamp(nowMs, timeZone);
171
189
  const mmdd = new Date(nowMs).toISOString().slice(5, 10);