crewly 1.20.308 → 1.20.310

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 (63) hide show
  1. package/README.md +2 -2
  2. package/config/skills/agent/remote-browser/SKILL.md +9 -4
  3. package/config/skills/agent/remote-browser/execute.sh +11 -1
  4. package/config/skills/agent/remote-browser/execute.test.sh +14 -0
  5. package/dist/backend/backend/src/constants.d.ts +20 -0
  6. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  7. package/dist/backend/backend/src/constants.js +21 -1
  8. package/dist/backend/backend/src/constants.js.map +1 -1
  9. package/dist/backend/backend/src/controllers/browser/browser.controller.js +1 -1
  10. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  11. package/dist/backend/backend/src/index.d.ts +7 -0
  12. package/dist/backend/backend/src/index.d.ts.map +1 -1
  13. package/dist/backend/backend/src/index.js +83 -0
  14. package/dist/backend/backend/src/index.js.map +1 -1
  15. package/dist/backend/backend/src/services/board/board-interaction-hooks.d.ts +34 -0
  16. package/dist/backend/backend/src/services/board/board-interaction-hooks.d.ts.map +1 -0
  17. package/dist/backend/backend/src/services/board/board-interaction-hooks.js +60 -0
  18. package/dist/backend/backend/src/services/board/board-interaction-hooks.js.map +1 -0
  19. package/dist/backend/backend/src/services/board/board-interactions.d.ts +54 -0
  20. package/dist/backend/backend/src/services/board/board-interactions.d.ts.map +1 -0
  21. package/dist/backend/backend/src/services/board/board-interactions.js +92 -0
  22. package/dist/backend/backend/src/services/board/board-interactions.js.map +1 -0
  23. package/dist/backend/backend/src/services/board/board-links.d.ts +51 -0
  24. package/dist/backend/backend/src/services/board/board-links.d.ts.map +1 -0
  25. package/dist/backend/backend/src/services/board/board-links.js +168 -0
  26. package/dist/backend/backend/src/services/board/board-links.js.map +1 -0
  27. package/dist/backend/backend/src/services/board/board-push.service.d.ts +8 -1
  28. package/dist/backend/backend/src/services/board/board-push.service.d.ts.map +1 -1
  29. package/dist/backend/backend/src/services/board/board-push.service.js +38 -1
  30. package/dist/backend/backend/src/services/board/board-push.service.js.map +1 -1
  31. package/dist/backend/backend/src/services/board/board-snapshot.builder.d.ts +32 -2
  32. package/dist/backend/backend/src/services/board/board-snapshot.builder.d.ts.map +1 -1
  33. package/dist/backend/backend/src/services/board/board-snapshot.builder.js +83 -8
  34. package/dist/backend/backend/src/services/board/board-snapshot.builder.js.map +1 -1
  35. package/dist/backend/backend/src/services/board/board.sources.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/board/board.sources.js +20 -1
  37. package/dist/backend/backend/src/services/board/board.sources.js.map +1 -1
  38. package/dist/backend/backend/src/services/board/board.types.d.ts +43 -0
  39. package/dist/backend/backend/src/services/board/board.types.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/board/board.types.js +9 -1
  41. package/dist/backend/backend/src/services/board/board.types.js.map +1 -1
  42. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +51 -1
  43. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
  44. package/dist/backend/backend/src/services/browser/browser-bridge.service.js +103 -6
  45. package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
  46. package/dist/backend/build-info.json +2 -2
  47. package/dist/cli/backend/src/constants.d.ts +20 -0
  48. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  49. package/dist/cli/backend/src/constants.js +21 -1
  50. package/dist/cli/backend/src/constants.js.map +1 -1
  51. package/dist/cli/cli/src/commands/start.d.ts +1 -0
  52. package/dist/cli/cli/src/commands/start.d.ts.map +1 -1
  53. package/dist/cli/cli/src/commands/start.js +13 -3
  54. package/dist/cli/cli/src/commands/start.js.map +1 -1
  55. package/dist/cli/cli/src/index.js +3 -2
  56. package/dist/cli/cli/src/index.js.map +1 -1
  57. package/dist/cli/cli/src/utils/open-dashboard.d.ts +38 -0
  58. package/dist/cli/cli/src/utils/open-dashboard.d.ts.map +1 -0
  59. package/dist/cli/cli/src/utils/open-dashboard.js +40 -0
  60. package/dist/cli/cli/src/utils/open-dashboard.js.map +1 -0
  61. package/frontend/dist/assets/{index-368419df.js → index-1b4ac286.js} +287 -287
  62. package/frontend/dist/index.html +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Where the board learns that one agent talked to another (canvas
3
+ * `messaging` links): a middleware on the terminal-write API (what the
4
+ * `send-message` skill calls) and a chat-message listener for agent messages
5
+ * that @mention another agent (rooms, DMs, Slack mirrored into chat).
6
+ * Only sender, receiver and time are kept.
7
+ *
8
+ * @module services/board/board-interaction-hooks
9
+ */
10
+ import type { NextFunction, Request, Response } from 'express';
11
+ import { type AgentInteractionRecorder } from './board-interactions.js';
12
+ /**
13
+ * Middleware (mounted on `/api`): an agent's successful write to another
14
+ * agent's terminal is a message.
15
+ *
16
+ * @param recorder - Where to record (default: process-wide)
17
+ * @returns Express middleware; never blocks or throws
18
+ */
19
+ export declare function recordTerminalWrites(recorder?: AgentInteractionRecorder): (req: Request, res: Response, next: NextFunction) => void;
20
+ /** The parts of a chat message the board reads. */
21
+ export interface MentionMessage {
22
+ senderType: string;
23
+ senderId: string;
24
+ mentions?: string[];
25
+ }
26
+ /**
27
+ * An agent's chat message that @mentions other agents.
28
+ *
29
+ * @param message - Chat message
30
+ * @param resolve - Member id or session -> session (undefined for a team or the owner)
31
+ * @param recorder - Where to record
32
+ */
33
+ export declare function recordChatMentions(message: MentionMessage, resolve: (idOrSession: string) => string | undefined, recorder?: AgentInteractionRecorder): void;
34
+ //# sourceMappingURL=board-interaction-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-interaction-hooks.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-interaction-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAAwB,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAI9F;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,wBAAiD,IACvF,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAmB9D;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAAE,QAAQ,GAAE,wBAAiD,GAAG,IAAI,CAOnL"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Where the board learns that one agent talked to another (canvas
3
+ * `messaging` links): a middleware on the terminal-write API (what the
4
+ * `send-message` skill calls) and a chat-message listener for agent messages
5
+ * that @mention another agent (rooms, DMs, Slack mirrored into chat).
6
+ * Only sender, receiver and time are kept.
7
+ *
8
+ * @module services/board/board-interaction-hooks
9
+ */
10
+ import { readAgentSessionHeader } from '../../utils/agent-caller.utils.js';
11
+ import { getAgentInteractions } from './board-interactions.js';
12
+ const WRITE_PATH = /^\/terminal\/([^/]+)\/write\/?$/;
13
+ /**
14
+ * Middleware (mounted on `/api`): an agent's successful write to another
15
+ * agent's terminal is a message.
16
+ *
17
+ * @param recorder - Where to record (default: process-wide)
18
+ * @returns Express middleware; never blocks or throws
19
+ */
20
+ export function recordTerminalWrites(recorder = getAgentInteractions()) {
21
+ return (req, res, next) => {
22
+ try {
23
+ if (req.method === 'POST') {
24
+ const match = WRITE_PATH.exec(req.path);
25
+ if (match) {
26
+ const from = readAgentSessionHeader(req);
27
+ const to = decodeURIComponent(match[1]);
28
+ if (from && to && from !== to) {
29
+ res.once('finish', () => {
30
+ if (res.statusCode >= 200 && res.statusCode < 300)
31
+ recorder.record(from, to);
32
+ });
33
+ }
34
+ }
35
+ }
36
+ }
37
+ catch {
38
+ /* observing never breaks the request */
39
+ }
40
+ next();
41
+ };
42
+ }
43
+ /**
44
+ * An agent's chat message that @mentions other agents.
45
+ *
46
+ * @param message - Chat message
47
+ * @param resolve - Member id or session -> session (undefined for a team or the owner)
48
+ * @param recorder - Where to record
49
+ */
50
+ export function recordChatMentions(message, resolve, recorder = getAgentInteractions()) {
51
+ if (message.senderType !== 'agent' || !message.mentions || message.mentions.length === 0)
52
+ return;
53
+ const from = resolve(message.senderId) ?? message.senderId;
54
+ for (const m of message.mentions.slice(0, 10)) {
55
+ const to = resolve(m);
56
+ if (to && to !== from)
57
+ recorder.record(from, to);
58
+ }
59
+ }
60
+ //# sourceMappingURL=board-interaction-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-interaction-hooks.js","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-interaction-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAiC,MAAM,yBAAyB,CAAC;AAE9F,MAAM,UAAU,GAAG,iCAAiC,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAAqC,oBAAoB,EAAE;IAC/F,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAChE,IAAI,CAAC;YACJ,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;gBAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACxC,IAAI,KAAK,EAAE,CAAC;oBACX,MAAM,IAAI,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC;oBACzC,MAAM,EAAE,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;oBACxC,IAAI,IAAI,IAAI,EAAE,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;wBAC/B,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE;4BACvB,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG;gCAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;wBAC9E,CAAC,CAAC,CAAC;oBACJ,CAAC;gBACF,CAAC;YACF,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,wCAAwC;QACzC,CAAC;QACD,IAAI,EAAE,CAAC;IACR,CAAC,CAAC;AACH,CAAC;AASD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAuB,EAAE,OAAoD,EAAE,WAAqC,oBAAoB,EAAE;IAC5K,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACjG,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC;IAC3D,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;QAC/C,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,EAAE,IAAI,EAAE,KAAK,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;AACF,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Who messaged whom (crewly#1247 canvas links): a small in-memory record of
3
+ * agent-to-agent messages, kept for the last two hours. The board turns it into
4
+ * `messaging` links. Contents are never kept, only sender, receiver and time.
5
+ *
6
+ * @module services/board/board-interactions
7
+ */
8
+ /** One sender -> receiver pair as the board reads it. */
9
+ export interface InteractionPair {
10
+ from: string;
11
+ to: string;
12
+ /** Messages in the window */
13
+ count: number;
14
+ firstAt: number;
15
+ lastAt: number;
16
+ }
17
+ /** Records agent-to-agent messages. */
18
+ export declare class AgentInteractionRecorder {
19
+ private readonly pairs;
20
+ private readonly listeners;
21
+ /**
22
+ * Note a message.
23
+ *
24
+ * @param from - Sender session (`<session>@<instanceId>` when on another machine)
25
+ * @param to - Receiver session
26
+ * @param now - Clock (tests)
27
+ */
28
+ record(from: string, to: string, now?: number): void;
29
+ /**
30
+ * Be told about each message (the push marks the sender dirty).
31
+ *
32
+ * @param listener - Called with sender and receiver
33
+ * @returns Unsubscribe
34
+ */
35
+ onRecord(listener: (from: string, to: string) => void): () => void;
36
+ /**
37
+ * Pairs with a message in the window; older entries are forgotten.
38
+ *
39
+ * @param now - Clock
40
+ * @returns Pairs
41
+ */
42
+ pairsNow(now?: number): InteractionPair[];
43
+ /** Forget everything (tests). */
44
+ clear(): void;
45
+ }
46
+ /**
47
+ * The process-wide recorder.
48
+ *
49
+ * @returns The recorder
50
+ */
51
+ export declare function getAgentInteractions(): AgentInteractionRecorder;
52
+ /** Drop the process-wide recorder (tests). */
53
+ export declare function resetAgentInteractions(): void;
54
+ //# sourceMappingURL=board-interactions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-interactions.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-interactions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,uCAAuC;AACvC,qBAAa,wBAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoE;IAC1F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAE3E;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI;IAmBhE;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAKlE;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,GAAE,MAAmB,GAAG,eAAe,EAAE;IAerD,iCAAiC;IACjC,KAAK,IAAI,IAAI;CAGb;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,wBAAwB,CAG/D;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Who messaged whom (crewly#1247 canvas links): a small in-memory record of
3
+ * agent-to-agent messages, kept for the last two hours. The board turns it into
4
+ * `messaging` links. Contents are never kept, only sender, receiver and time.
5
+ *
6
+ * @module services/board/board-interactions
7
+ */
8
+ import { BOARD_CONSTANTS } from '../../constants.js';
9
+ const C = BOARD_CONSTANTS;
10
+ /** Records agent-to-agent messages. */
11
+ export class AgentInteractionRecorder {
12
+ pairs = new Map();
13
+ listeners = new Set();
14
+ /**
15
+ * Note a message.
16
+ *
17
+ * @param from - Sender session (`<session>@<instanceId>` when on another machine)
18
+ * @param to - Receiver session
19
+ * @param now - Clock (tests)
20
+ */
21
+ record(from, to, now = Date.now()) {
22
+ if (!from || !to || from === to)
23
+ return;
24
+ const key = `${from}\u0000${to}`;
25
+ let pair = this.pairs.get(key);
26
+ if (!pair) {
27
+ pair = { from, to, times: [] };
28
+ this.pairs.set(key, pair);
29
+ }
30
+ pair.times.push(now);
31
+ if (pair.times.length > C.LINK_MESSAGING_MAX_PER_PAIR)
32
+ pair.times.splice(0, pair.times.length - C.LINK_MESSAGING_MAX_PER_PAIR);
33
+ for (const l of this.listeners) {
34
+ try {
35
+ l(from, to);
36
+ }
37
+ catch {
38
+ /* a listener never breaks the sender */
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Be told about each message (the push marks the sender dirty).
44
+ *
45
+ * @param listener - Called with sender and receiver
46
+ * @returns Unsubscribe
47
+ */
48
+ onRecord(listener) {
49
+ this.listeners.add(listener);
50
+ return () => this.listeners.delete(listener);
51
+ }
52
+ /**
53
+ * Pairs with a message in the window; older entries are forgotten.
54
+ *
55
+ * @param now - Clock
56
+ * @returns Pairs
57
+ */
58
+ pairsNow(now = Date.now()) {
59
+ const out = [];
60
+ const cutoff = now - C.LINK_MESSAGING_WINDOW_MS;
61
+ for (const [key, pair] of this.pairs) {
62
+ const first = pair.times.findIndex((t) => t >= cutoff);
63
+ if (first < 0) {
64
+ this.pairs.delete(key);
65
+ continue;
66
+ }
67
+ if (first > 0)
68
+ pair.times.splice(0, first);
69
+ out.push({ from: pair.from, to: pair.to, count: pair.times.length, firstAt: pair.times[0], lastAt: pair.times[pair.times.length - 1] });
70
+ }
71
+ return out;
72
+ }
73
+ /** Forget everything (tests). */
74
+ clear() {
75
+ this.pairs.clear();
76
+ }
77
+ }
78
+ let instance = null;
79
+ /**
80
+ * The process-wide recorder.
81
+ *
82
+ * @returns The recorder
83
+ */
84
+ export function getAgentInteractions() {
85
+ instance ??= new AgentInteractionRecorder();
86
+ return instance;
87
+ }
88
+ /** Drop the process-wide recorder (tests). */
89
+ export function resetAgentInteractions() {
90
+ instance = null;
91
+ }
92
+ //# sourceMappingURL=board-interactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-interactions.js","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-interactions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Canvas links (crewly#1247, CANVAS-V2-CONTRACT.md): who reports to whom and
3
+ * who works with whom. Pure functions of what the board already read, so a
4
+ * delta never touches the disk.
5
+ *
6
+ * @module services/board/board-links
7
+ */
8
+ import type { InteractionPair } from './board-interactions.js';
9
+ import type { BoardTeamInfo, BoardTicket, BoardWorkItem } from './board-snapshot.builder.js';
10
+ import { type BoardLink } from './board.types.js';
11
+ /** What the structural links read. */
12
+ export interface StructuralLinkInput {
13
+ now: number;
14
+ orchestrator: string;
15
+ teams: BoardTeamInfo[];
16
+ workItems: BoardWorkItem[];
17
+ tickets: BoardTicket[];
18
+ /** Each agent's current task (a WorkItem id or a ticket id) */
19
+ tasks: Map<string, {
20
+ id?: string;
21
+ title: string;
22
+ since: string;
23
+ }>;
24
+ /** Sessions that are stopped (their reporting line is not active) */
25
+ stopped: ReadonlySet<string>;
26
+ }
27
+ /**
28
+ * Reporting, delegation and shared-ticket links.
29
+ *
30
+ * @param input - Teams, work and tickets
31
+ * @returns Links, one per identity
32
+ */
33
+ export declare function structuralLinks(input: StructuralLinkInput): BoardLink[];
34
+ /**
35
+ * Messaging links from the recorder.
36
+ *
37
+ * @param pairs - Sender/receiver pairs in the last two hours
38
+ * @param now - Clock
39
+ * @param isKnown - Whether an endpoint is an agent of this machine or qualified
40
+ * @returns Links
41
+ */
42
+ export declare function messagingLinks(pairs: readonly InteractionPair[], now: number, isKnown: (session: string) => boolean): BoardLink[];
43
+ /**
44
+ * Cap a link list: messaging, then same_task, are dropped first.
45
+ *
46
+ * @param links - Links
47
+ * @param max - Most links to keep
48
+ * @returns At most `max` links
49
+ */
50
+ export declare function capLinks(links: BoardLink[], max?: number): BoardLink[];
51
+ //# sourceMappingURL=board-links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-links.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-links.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7F,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAkB3D,sCAAsC;AACtC,MAAM,WAAW,mBAAmB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,+DAA+D;IAC/D,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qEAAqE;IACrE,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,EAAE,CA6FvE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,EAAE,CAOjI;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,GAAG,GAAE,MAAoB,GAAG,SAAS,EAAE,CAMnF"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Canvas links (crewly#1247, CANVAS-V2-CONTRACT.md): who reports to whom and
3
+ * who works with whom. Pure functions of what the board already read, so a
4
+ * delta never touches the disk.
5
+ *
6
+ * @module services/board/board-links
7
+ */
8
+ import { BOARD_CONSTANTS } from '../../constants.js';
9
+ import { scrubText } from './board-redact.js';
10
+ import { linkKey } from './board.types.js';
11
+ const C = BOARD_CONSTANTS;
12
+ /** Work states that no longer count as open. */
13
+ const CLOSED = new Set(['done', 'verified', 'cancelled', 'rejected', 'failed']);
14
+ /** Closed states that still show as finished delegations. */
15
+ const FINISHED_OK = new Set(['done', 'verified']);
16
+ const iso = (ms) => new Date(ms).toISOString();
17
+ const msOf = (s) => (s ? Date.parse(s) : NaN);
18
+ /** The label of a link: scrubbed, short. */
19
+ function label(text) {
20
+ const t = text ? scrubText(text, C.LINK_LABEL_MAX) : '';
21
+ return t ? t.slice(0, C.LINK_LABEL_MAX) : undefined;
22
+ }
23
+ /**
24
+ * Reporting, delegation and shared-ticket links.
25
+ *
26
+ * @param input - Teams, work and tickets
27
+ * @returns Links, one per identity
28
+ */
29
+ export function structuralLinks(input) {
30
+ const { now, orchestrator, teams, workItems, tickets, tasks, stopped } = input;
31
+ const out = new Map();
32
+ const put = (l) => {
33
+ if (l.from === l.to)
34
+ return;
35
+ out.set(linkKey(l), l);
36
+ };
37
+ const known = new Set([orchestrator]);
38
+ const teamOf = new Map();
39
+ for (const t of teams)
40
+ for (const m of t.members) {
41
+ known.add(m.session);
42
+ teamOf.set(m.session, t);
43
+ }
44
+ const isKnown = (s) => known.has(s) || s.includes('@');
45
+ // --- reports_to: members -> their leaders, leaders -> the orchestrator
46
+ for (const t of teams) {
47
+ if (t.teamId === 'orchestrator')
48
+ continue;
49
+ const leaders = (t.leaders ?? []).filter((s) => t.members.some((m) => m.session === s));
50
+ const since = t.createdAt ?? iso(now);
51
+ for (const m of t.members) {
52
+ if (leaders.includes(m.session)) {
53
+ put({ from: m.session, to: orchestrator, kind: 'reports_to', active: !stopped.has(m.session), since });
54
+ }
55
+ else {
56
+ for (const lead of leaders)
57
+ put({ from: m.session, to: lead, kind: 'reports_to', active: !stopped.has(m.session), since });
58
+ }
59
+ }
60
+ }
61
+ // --- delegated: delegator -> worker, per pair
62
+ const windowStart = now - C.LINK_DELEGATION_WINDOW_MS;
63
+ const delegations = new Map();
64
+ for (const w of workItems) {
65
+ const to = w.target;
66
+ if (!to || !isKnown(to))
67
+ continue;
68
+ let from = w.delegatedBy;
69
+ if (!from) {
70
+ if (w.owner === 'orchestrator')
71
+ from = orchestrator;
72
+ else if (w.owner === 'team_lead')
73
+ from = teamOf.get(to)?.leaders?.find((s) => s !== to);
74
+ }
75
+ if (!from || from === to || !isKnown(from))
76
+ continue;
77
+ const open = !CLOSED.has(w.status);
78
+ const created = msOf(w.createdAt);
79
+ if (!open) {
80
+ const done = msOf(w.completedAt) || msOf(w.statusChangedAt) || created;
81
+ if (!FINISHED_OK.has(w.status) || !(done >= windowStart))
82
+ continue;
83
+ }
84
+ const key = `${from}\u0000${to}`;
85
+ const entry = delegations.get(key) ?? { from, to, items: [] };
86
+ entry.items.push({ w, open, at: Number.isFinite(created) ? created : now });
87
+ delegations.set(key, entry);
88
+ }
89
+ for (const { from, to, items } of delegations.values()) {
90
+ const open = items.filter((i) => i.open);
91
+ const pool = open.length > 0 ? open : items;
92
+ const newest = pool.reduce((a, b) => (b.at > a.at ? b : a));
93
+ put({ from, to, kind: 'delegated', active: open.length > 0, since: iso(newest.at), ...(label(newest.w.title) ? { label: label(newest.w.title) } : {}), weight: items.length });
94
+ }
95
+ // --- same_task: two agents on one ticket right now
96
+ const ticketTitle = new Map();
97
+ for (const t of tickets) {
98
+ ticketTitle.set(t.id, t.label ? `${t.label} ${t.title}` : t.title);
99
+ if (t.label)
100
+ ticketTitle.set(t.label, `${t.label} ${t.title}`);
101
+ }
102
+ const groups = new Map();
103
+ const join = (key, session, since) => {
104
+ const g = groups.get(key) ?? new Map();
105
+ g.set(session, Math.max(g.get(session) ?? 0, Number.isFinite(since) ? since : now));
106
+ groups.set(key, g);
107
+ };
108
+ for (const w of workItems) {
109
+ if (w.status !== 'running' || !w.target || !isKnown(w.target))
110
+ continue;
111
+ const key = w.projectTaskId ?? w.requestId;
112
+ if (key)
113
+ join(key, w.target, msOf(w.startedAt) || msOf(w.createdAt));
114
+ }
115
+ for (const [session, task] of tasks) {
116
+ if (task.id && ticketTitle.has(task.id) && isKnown(session))
117
+ join(task.id, session, msOf(task.since));
118
+ }
119
+ for (const [key, members] of groups) {
120
+ if (members.size < 2)
121
+ continue;
122
+ const sessions = [...members.keys()].sort().slice(0, C.LINK_SAME_TASK_MAX_GROUP);
123
+ const text = label(ticketTitle.get(key) ?? workItems.find((w) => w.projectTaskId === key || w.requestId === key)?.title);
124
+ for (let i = 0; i < sessions.length; i += 1) {
125
+ for (let j = i + 1; j < sessions.length; j += 1) {
126
+ const since = Math.max(members.get(sessions[i]) ?? now, members.get(sessions[j]) ?? now);
127
+ const link = { from: sessions[i], to: sessions[j], kind: 'same_task', active: true, since: iso(since), ...(text ? { label: text } : {}) };
128
+ // Two tickets shared by one pair: the earlier key keeps the pair
129
+ if (!out.has(linkKey(link)))
130
+ put(link);
131
+ }
132
+ }
133
+ }
134
+ return [...out.values()];
135
+ }
136
+ /**
137
+ * Messaging links from the recorder.
138
+ *
139
+ * @param pairs - Sender/receiver pairs in the last two hours
140
+ * @param now - Clock
141
+ * @param isKnown - Whether an endpoint is an agent of this machine or qualified
142
+ * @returns Links
143
+ */
144
+ export function messagingLinks(pairs, now, isKnown) {
145
+ const out = [];
146
+ for (const p of pairs) {
147
+ if (!isKnown(p.from) || !isKnown(p.to))
148
+ continue;
149
+ out.push({ from: p.from, to: p.to, kind: 'messaging', active: now - p.lastAt <= C.LINK_MESSAGING_ACTIVE_MS, since: iso(p.firstAt), weight: p.count });
150
+ }
151
+ return out;
152
+ }
153
+ /**
154
+ * Cap a link list: messaging, then same_task, are dropped first.
155
+ *
156
+ * @param links - Links
157
+ * @param max - Most links to keep
158
+ * @returns At most `max` links
159
+ */
160
+ export function capLinks(links, max = C.LINKS_MAX) {
161
+ if (links.length <= max)
162
+ return links;
163
+ const rank = { reports_to: 0, delegated: 1, same_task: 2, messaging: 3 };
164
+ return [...links]
165
+ .sort((a, b) => rank[a.kind] - rank[b.kind] || Number(b.active) - Number(a.active) || Date.parse(b.since) - Date.parse(a.since))
166
+ .slice(0, max);
167
+ }
168
+ //# sourceMappingURL=board-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-links.js","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-links.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,8 @@
21
21
  import { type ComponentLogger } from '../core/logger.service.js';
22
22
  import type { BoardActivityTracker } from './board-activity.tracker.js';
23
23
  import type { BoardSnapshotBuilder } from './board-snapshot.builder.js';
24
- import type { BoardSnapshot } from './board.types.js';
24
+ import { type AgentInteractionRecorder } from './board-interactions.js';
25
+ import { type BoardSnapshot } from './board.types.js';
25
26
  /** The Cloud session (CloudClientService). */
26
27
  export interface BoardCloudSession {
27
28
  getToken(): string | null;
@@ -39,6 +40,8 @@ export interface BoardPushDeps {
39
40
  setTimeout?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
40
41
  clearTimeout?: (handle: ReturnType<typeof setTimeout>) => void;
41
42
  logger?: ComponentLogger;
43
+ /** Agent-to-agent messages (default: the process-wide recorder) */
44
+ interactions?: AgentInteractionRecorder;
42
45
  }
43
46
  /** What one push did. */
44
47
  export type BoardPushOutcome = 'sent' | 'nothing' | 'skipped' | 'paused' | 'failed';
@@ -72,6 +75,10 @@ export declare class BoardPushService {
72
75
  private readonly dirty;
73
76
  /** Rows Cloud has, as JSON, by session */
74
77
  private readonly lastSent;
78
+ /** Links Cloud has, as JSON, by identity */
79
+ private readonly lastLinks;
80
+ /** A message was recorded since the last delta: the links may differ */
81
+ private linksDirty;
75
82
  private lastToken;
76
83
  private fullOk;
77
84
  private fullAfterMs;
@@ -1 +1 @@
1
- {"version":3,"file":"board-push.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-push.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"board-push.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/board/board-push.service.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,8 @@
21
21
  import { gzipSync } from 'zlib';
22
22
  import { BOARD_CONSTANTS, CANVAS_CONSTANTS } from '../../constants.js';
23
23
  import { LoggerService } from '../core/logger.service.js';
24
+ import { getAgentInteractions } from './board-interactions.js';
25
+ import { linkKey } from './board.types.js';
24
26
  const C = BOARD_CONSTANTS;
25
27
  const CV = CANVAS_CONSTANTS;
26
28
  /**
@@ -42,6 +44,9 @@ export function isBoardPushDisabled(env = process.env) {
42
44
  */
43
45
  export function fitSnapshot(snapshot) {
44
46
  const size = () => Buffer.byteLength(JSON.stringify(snapshot));
47
+ // Links go first: a map of lines is the least important part of the body
48
+ while (snapshot.links && snapshot.links.length > 0 && size() > C.MAX_BODY_BYTES)
49
+ snapshot.links.length = Math.floor(snapshot.links.length / 2);
45
50
  const lists = [snapshot.outcomes.doneToday, snapshot.outcomes.stuck, snapshot.outcomes.waitingOnOwner];
46
51
  let guard = 0;
47
52
  while (size() > C.MAX_BODY_BYTES && guard < 500) {
@@ -74,6 +79,10 @@ export class BoardPushService {
74
79
  dirty = new Set();
75
80
  /** Rows Cloud has, as JSON, by session */
76
81
  lastSent = new Map();
82
+ /** Links Cloud has, as JSON, by identity */
83
+ lastLinks = new Map();
84
+ /** A message was recorded since the last delta: the links may differ */
85
+ linksDirty = false;
77
86
  lastToken = null;
78
87
  fullOk = false;
79
88
  fullAfterMs = C.FULL_INTERVAL_MS;
@@ -124,9 +133,14 @@ export class BoardPushService {
124
133
  // A step changed: the agent's row did not necessarily, but a delta is due
125
134
  const offRows = this.deps.tracker.onChange((session) => this.markDirty(session));
126
135
  const offSteps = this.deps.tracker.steps.onChange((session) => this.markDirty(session));
136
+ const offMessages = (this.deps.interactions ?? getAgentInteractions()).onRecord((from) => {
137
+ this.linksDirty = true;
138
+ this.markDirty(from);
139
+ });
127
140
  this.unsubscribe = () => {
128
141
  offRows();
129
142
  offSteps();
143
+ offMessages();
130
144
  };
131
145
  this.scheduleFull(0);
132
146
  }
@@ -218,6 +232,10 @@ export class BoardPushService {
218
232
  for (const team of snapshot.teams)
219
233
  for (const a of team.agents)
220
234
  this.lastSent.set(a.session, JSON.stringify(a));
235
+ this.lastLinks.clear();
236
+ for (const l of snapshot.links ?? [])
237
+ this.lastLinks.set(linkKey(l), JSON.stringify(l));
238
+ this.linksDirty = false;
221
239
  this.dirty.clear();
222
240
  // Timeline events ride in deltas
223
241
  const pending = new Set([...this.deps.tracker.sessionsWithUnsent(), ...this.deps.tracker.steps.sessionsWithUnsent()]);
@@ -249,7 +267,18 @@ export class BoardPushService {
249
267
  const changed = rows.filter((r) => this.lastSent.get(r.session) !== JSON.stringify(stripTeam(r)));
250
268
  const timeline = this.deps.tracker.peekUnsent(sessions);
251
269
  let steps = this.deps.tracker.steps.peekUnsent(sessions);
252
- if (changed.length === 0 && timeline.length === 0 && steps.length === 0)
270
+ // Links are recomputed only after a message (the rest comes with the full build)
271
+ let links = [];
272
+ let removedLinks = [];
273
+ const linksWereDirty = this.linksDirty;
274
+ if (linksWereDirty) {
275
+ this.linksDirty = false;
276
+ const now = this.deps.builder.links();
277
+ links = now.filter((l) => this.lastLinks.get(linkKey(l)) !== JSON.stringify(l));
278
+ const keys = new Set(now.map(linkKey));
279
+ removedLinks = [...this.lastLinks.keys()].filter((k) => !keys.has(k));
280
+ }
281
+ if (changed.length === 0 && timeline.length === 0 && steps.length === 0 && links.length === 0 && removedLinks.length === 0)
253
282
  return 'nothing';
254
283
  const instanceId = (await this.deps.builder.identity()).instanceId;
255
284
  const body = {
@@ -259,6 +288,8 @@ export class BoardPushService {
259
288
  agents: changed,
260
289
  ...(timeline.length > 0 ? { timeline } : {}),
261
290
  ...(steps.length > 0 ? { steps } : {}),
291
+ ...(links.length > 0 ? { links } : {}),
292
+ ...(removedLinks.length > 0 ? { removedLinks } : {}),
262
293
  };
263
294
  // The body limit is on the uncompressed JSON: the newest steps go first when it does not fit
264
295
  for (let guard = 0; guard < 20 && body.steps && Buffer.byteLength(JSON.stringify(body)) > C.MAX_BODY_BYTES; guard += 1) {
@@ -272,6 +303,10 @@ export class BoardPushService {
272
303
  if (outcome === 'sent') {
273
304
  for (const r of changed)
274
305
  this.lastSent.set(r.session, JSON.stringify(stripTeam(r)));
306
+ for (const l of links)
307
+ this.lastLinks.set(linkKey(l), JSON.stringify(l));
308
+ for (const k of removedLinks)
309
+ this.lastLinks.delete(k);
275
310
  this.deps.tracker.ackUnsent(timeline);
276
311
  this.deps.tracker.steps.ackUnsent(steps);
277
312
  // Steps the per-delta limit held back go in the next one
@@ -281,6 +316,8 @@ export class BoardPushService {
281
316
  }
282
317
  else if (outcome === 'failed') {
283
318
  // keep them dirty for the next try (the full push also repairs everything)
319
+ if (linksWereDirty)
320
+ this.linksDirty = true;
284
321
  for (const s of sessions)
285
322
  this.dirty.add(s);
286
323
  }