group-chat-mcp 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/STEPS.md +43 -0
- package/dist/__tests__/cli-session-commands.test.js +15 -22
- package/dist/__tests__/cli-session-commands.test.js.map +1 -1
- package/dist/__tests__/send-lock.test.d.ts +2 -0
- package/dist/__tests__/send-lock.test.d.ts.map +1 -0
- package/dist/__tests__/send-lock.test.js +239 -0
- package/dist/__tests__/send-lock.test.js.map +1 -0
- package/dist/__tests__/tool-handlers.test.d.ts +2 -0
- package/dist/__tests__/tool-handlers.test.d.ts.map +1 -0
- package/dist/__tests__/tool-handlers.test.js +102 -0
- package/dist/__tests__/tool-handlers.test.js.map +1 -0
- package/dist/gchat.d.ts.map +1 -1
- package/dist/gchat.js +6 -4
- package/dist/gchat.js.map +1 -1
- package/dist/index.js +21 -8
- package/dist/index.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +13 -13
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +5 -4
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/dist/services/state-service.d.ts +1 -0
- package/dist/services/state-service.d.ts.map +1 -1
- package/dist/services/state-service.js +15 -0
- package/dist/services/state-service.js.map +1 -1
- package/dist/services/tool-handlers.d.ts.map +1 -1
- package/dist/services/tool-handlers.js +91 -20
- package/dist/services/tool-handlers.js.map +1 -1
- package/dist/types/agent.d.ts +1 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/notification.d.ts +1 -0
- package/dist/types/notification.d.ts.map +1 -1
- package/dist/utils/file-lock.d.ts +2 -0
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +2 -2
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/notification-utils.d.ts +5 -1
- package/dist/utils/notification-utils.d.ts.map +1 -1
- package/dist/utils/notification-utils.js +25 -6
- package/dist/utils/notification-utils.js.map +1 -1
- package/dist/utils/send-lock.d.ts +24 -0
- package/dist/utils/send-lock.d.ts.map +1 -0
- package/dist/utils/send-lock.js +139 -0
- package/dist/utils/send-lock.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.6] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Per-conversation send lock that serializes concurrent `send_message` calls within a conversation, returning competing messages with reconsideration instructions to blocked agents
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Send lock robustness: stale lock detection with process liveness checks, lock release on agent disconnect, and jittered poll intervals to prevent filesystem contention
|
|
17
|
+
|
|
18
|
+
## [0.1.5] - 2026-03-22
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Deferred join announcements: agents no longer broadcast anonymous UUID-based join messages; announcements are deferred until `update_profile` is called, producing human-readable join messages with the agent's chosen name
|
|
23
|
+
- `hasAnnounced` per-conversation flag on agents to track announcement state
|
|
24
|
+
- `writeProfileSetupNotification` prompts joining agents to set their profile when entering a multi-participant conversation
|
|
25
|
+
- Profile reminder on `send_message` when a nameless agent messages a multi-participant conversation
|
|
26
|
+
- `agentName` field on notifications; `formatNotificationContent` prefers `agentName` over UUID for display
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `update_profile` now requires all four fields (`name`, `role`, `expertise`, `status`) as non-empty strings
|
|
31
|
+
- `writeNotificationToParticipants` signature changed to accept `opts` object with optional `excludeAgentId` and `agentName`
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Defensive initialization of `hasAnnounced` for agents loaded from pre-migration storage
|
|
36
|
+
- DM path no longer calls `setHasAnnounced` on every message (only on new DM creation)
|
|
37
|
+
- Profile setup notification skipped for agents that already have a profile name
|
|
38
|
+
- Solo conversation deferred join announcements skipped (no pointless system messages)
|
|
39
|
+
- Join notification formatting no longer produces redundant output when content is present
|
|
40
|
+
|
|
8
41
|
## [0.1.4] - 2026-03-22
|
|
9
42
|
|
|
10
43
|
### Changed
|
package/STEPS.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# End Goal
|
|
2
|
+
|
|
3
|
+
Add a per-conversation message-sending lock that serializes sends within a conversation. When an agent attempts to send while another agent is already sending, the server waits internally until the competing message lands, then returns that message with strict instructions to reconsider and resend — never discarding the blocked agent's intent.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
- [ ] Add a per-conversation send lock to the state layer
|
|
8
|
+
- [ ] File-based lock per conversation (reusing the existing lock primitive pattern) that tracks the sending agent's ID
|
|
9
|
+
- [ ] 10-second stale threshold with automatic lock breaking and process liveness check
|
|
10
|
+
- [ ] Integrate the lock into the send_message tool handler
|
|
11
|
+
- [ ] On lock contention: wait internally (poll) until the competing message lands
|
|
12
|
+
- [ ] On lock release: read the latest message(s) from the conversation file that arrived while waiting, return them with strict instructions to read, reconsider the original intent, and resend (do NOT echo the blocked agent's original message)
|
|
13
|
+
- [ ] On timeout: break the stale lock and proceed with the blocked agent's send normally
|
|
14
|
+
- [ ] Handle lock cleanup on agent disconnect/crash to prevent deadlocks
|
|
15
|
+
|
|
16
|
+
## Questions Answered
|
|
17
|
+
|
|
18
|
+
1. Q: Should the send lock be scoped per-conversation or global?
|
|
19
|
+
A: Per-conversation. Agents can send to different conversations simultaneously, but only one at a time per conversation.
|
|
20
|
+
|
|
21
|
+
2. Q: Should the server reject immediately or wait internally?
|
|
22
|
+
A: Wait internally until the competing message is sent, then return that message in the rejection response. This way the blocked agent immediately sees the message that beat it, can adjust its reply, and retry without waiting for a notification cycle.
|
|
23
|
+
|
|
24
|
+
3. Q: Should the blocked agent still receive the competing message via normal notifications?
|
|
25
|
+
A: Yes. The notification system stays unchanged. Duplicate exposure is acceptable and keeps the system simple.
|
|
26
|
+
|
|
27
|
+
4. Q: How long should the blocked agent wait before timing out?
|
|
28
|
+
A: 10 seconds. Matches existing stale lock threshold. Normal sends complete in milliseconds.
|
|
29
|
+
|
|
30
|
+
5. Q: What happens on timeout?
|
|
31
|
+
A: Break the stale lock and let the blocked agent send its message. Automatic recovery, no manual intervention.
|
|
32
|
+
|
|
33
|
+
6. Q: Should the rejection response echo the blocked agent's original message?
|
|
34
|
+
A: No. Only return the competing message. The agent must reconsider its message in light of what the other agent said — that's the entire point of this feature. Echoing the original would tempt it to just resend without reconsidering.
|
|
35
|
+
|
|
36
|
+
7. Q: When multiple agents are blocked on the same conversation, queue or race?
|
|
37
|
+
A: Race. All waiters re-contend when the lock releases. Losers wait again and see each subsequent competing message. Natural fit for file-based locks.
|
|
38
|
+
|
|
39
|
+
8. Q: How should the blocked agent retrieve the competing message?
|
|
40
|
+
A: Read from the conversation's message file after the lock releases. Simple, uses existing data. In a multi-waiter race, each agent sees all messages sent since it started waiting.
|
|
41
|
+
|
|
42
|
+
9. Q: Should the send_message tool description be updated to document the lock behavior?
|
|
43
|
+
A: No. Discovery only. Agents learn about it when they hit contention via the rejection response.
|
|
@@ -7,7 +7,6 @@ import { SessionStateService } from '../services/session-state-service.js';
|
|
|
7
7
|
import { handleCursorJoin, handleCursorLeave } from '../gchat.js';
|
|
8
8
|
import { ConversationType } from '../enums/conversation-type.js';
|
|
9
9
|
import { NotificationType } from '../enums/notification-type.js';
|
|
10
|
-
import { INBOXES_DIR } from '../constants/storage.js';
|
|
11
10
|
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
11
|
let tmpDir;
|
|
13
12
|
let stateService;
|
|
@@ -48,26 +47,28 @@ describe('cursor-join', () => {
|
|
|
48
47
|
expect(conversation).not.toBeNull();
|
|
49
48
|
expect(conversation.participants).toContain(result.agentId);
|
|
50
49
|
});
|
|
50
|
+
it('When cursor-join is called Then no system message is written to the conversation', async () => {
|
|
51
|
+
const projectPath = '/test-project';
|
|
52
|
+
const serverPid = 12345;
|
|
53
|
+
const result = await handleCursorJoin(projectPath, serverPid, { stateService, sessionStateService });
|
|
54
|
+
const messages = await stateService.getMessages(result.conversationId);
|
|
55
|
+
const systemMessages = messages.filter((m) => m.type === 'system');
|
|
56
|
+
expect(systemMessages).toHaveLength(0);
|
|
57
|
+
});
|
|
51
58
|
});
|
|
52
59
|
describe('Given project path with existing conversation and 1 participant', () => {
|
|
53
|
-
it('When cursor-join is called
|
|
60
|
+
it('When cursor-join is called for a conversation with existing participants Then the new agent inbox contains a profile setup notification', async () => {
|
|
54
61
|
const projectPath = '/test-project';
|
|
55
62
|
const agentA = await stateService.registerAgent(projectPath);
|
|
56
63
|
const conversation = await stateService.getOrCreateProjectConversation(projectPath);
|
|
57
64
|
await stateService.joinConversation(agentA.id, conversation.id);
|
|
58
65
|
const serverPidB = 54321;
|
|
59
66
|
const result = await handleCursorJoin(projectPath, serverPidB, { stateService, sessionStateService });
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
notifications = [];
|
|
68
|
-
}
|
|
69
|
-
const joinNotification = notifications.find((n) => n.type === NotificationType.Join && n.agentId === result.agentId);
|
|
70
|
-
expect(joinNotification).toBeDefined();
|
|
67
|
+
const joiningNotifications = await stateService.getInbox(result.agentId);
|
|
68
|
+
const profileSetupNotification = joiningNotifications.find((n) => n.type === NotificationType.Join && n.content.includes('Update your profile'));
|
|
69
|
+
expect(profileSetupNotification).toBeDefined();
|
|
70
|
+
const existingNotifications = await stateService.getInbox(agentA.id);
|
|
71
|
+
expect(existingNotifications).toHaveLength(0);
|
|
71
72
|
});
|
|
72
73
|
});
|
|
73
74
|
});
|
|
@@ -97,15 +98,7 @@ describe('cursor-leave', () => {
|
|
|
97
98
|
const resultY = await handleCursorJoin(projectPath, serverPidY, { stateService, sessionStateService });
|
|
98
99
|
const resultX = await handleCursorJoin(projectPath, serverPidX, { stateService, sessionStateService });
|
|
99
100
|
await handleCursorLeave(serverPidX, { stateService, sessionStateService });
|
|
100
|
-
const
|
|
101
|
-
let notifications = [];
|
|
102
|
-
try {
|
|
103
|
-
const raw = await fs.readFile(inboxPath, 'utf-8');
|
|
104
|
-
notifications = JSON.parse(raw);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
notifications = [];
|
|
108
|
-
}
|
|
101
|
+
const notifications = await stateService.getInbox(resultY.agentId);
|
|
109
102
|
const leaveNotification = notifications.find((n) => n.type === NotificationType.Leave && n.agentId === resultX.agentId);
|
|
110
103
|
expect(leaveNotification).toBeDefined();
|
|
111
104
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-session-commands.test.js","sourceRoot":"","sources":["../../src/__tests__/cli-session-commands.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-session-commands.test.js","sourceRoot":"","sources":["../../src/__tests__/cli-session-commands.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,MAAM,aAAa,GAAG,iEAAiE,CAAC;AAExF,IAAI,MAAc,CAAC;AACnB,IAAI,YAA0B,CAAC;AAC/B,IAAI,mBAAwC,CAAC;AAE7C,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACrE,YAAY,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,YAAY,CAAC,IAAI,EAAE,CAAC;IAC1B,mBAAmB,GAAG,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;QACjD,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;YAChG,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,KAAK,CAAC;YAExB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAErG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAC9C,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sFAAsF,EAAE,KAAK,IAAI,EAAE;YACpG,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,KAAK,CAAC;YAExB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAErG,MAAM,eAAe,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACpE,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,CAAC,eAAgB,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAEvD,MAAM,aAAa,GAAG,MAAM,mBAAmB,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAC5E,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;YACtG,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,KAAK,CAAC;YAExB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAErG,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,eAAe,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YAC/E,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YACpC,MAAM,CAAC,YAAa,CAAC,YAAY,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;YAChG,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,KAAK,CAAC;YAExB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAErG,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YACvE,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;YACnE,MAAM,CAAC,cAAc,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,iEAAiE,EAAE,GAAG,EAAE;QAC/E,EAAE,CAAC,yIAAyI,EAAE,KAAK,IAAI,EAAE;YACvJ,MAAM,WAAW,GAAG,eAAe,CAAC;YAEpC,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;YAC7D,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,8BAA8B,CAAC,WAAW,CAAC,CAAC;YACpF,MAAM,YAAY,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;YAEhE,MAAM,UAAU,GAAG,KAAK,CAAC;YACzB,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAEtG,MAAM,oBAAoB,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAEzE,MAAM,wBAAwB,GAAG,oBAAoB,CAAC,IAAI,CACxD,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,gBAAgB,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,CACrF,CAAC;YACF,MAAM,CAAC,wBAAwB,CAAC,CAAC,WAAW,EAAE,CAAC;YAE/C,MAAM,qBAAqB,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAErE,MAAM,CAAC,qBAAqB,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAChD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,QAAQ,CAAC,wCAAwC,EAAE,GAAG,EAAE;QACtD,EAAE,CAAC,iFAAiF,EAAE,KAAK,IAAI,EAAE;YAC/F,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,IAAI,CAAC;YAEvB,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAEzG,MAAM,iBAAiB,CAAC,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAE1E,MAAM,eAAe,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YACxE,MAAM,CAAC,eAAe,CAAC,CAAC,QAAQ,EAAE,CAAC;YAEnC,MAAM,aAAa,GAAG,MAAM,mBAAmB,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAC5E,MAAM,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qCAAqC,EAAE,GAAG,EAAE;QACnD,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;YACnE,MAAM,MAAM,CACV,iBAAiB,CAAC,IAAI,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAC/D,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;QAC1D,EAAE,CAAC,gFAAgF,EAAE,KAAK,IAAI,EAAE;YAC9F,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,UAAU,GAAG,IAAI,CAAC;YACxB,MAAM,UAAU,GAAG,IAAI,CAAC;YAExB,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACvG,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAEvG,MAAM,iBAAiB,CAAC,UAAU,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAE3E,MAAM,aAAa,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAEnE,MAAM,iBAAiB,GAAG,aAAa,CAAC,IAAI,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,gBAAgB,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,CAC1E,CAAC;YACF,MAAM,CAAC,iBAAiB,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAChE,EAAE,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;YACtG,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,IAAI,CAAC;YAEvB,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAEzG,MAAM,kBAAkB,GAAG,MAAM,YAAY,CAAC,kBAAkB,CAAC;gBAC/D,IAAI,EAAE,gBAAgB,CAAC,KAAK;gBAC5B,IAAI,EAAE,qBAAqB;gBAC3B,YAAY,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;aACnC,CAAC,CAAC;YAEH,MAAM,iBAAiB,CAAC,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAE1E,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC9D,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;YAEzB,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;YAC5E,MAAM,CAAC,KAAM,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAE9D,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,eAAe,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC;YACxE,MAAM,CAAC,KAAM,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qDAAqD,EAAE,GAAG,EAAE;QACnE,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;YAC3E,MAAM,WAAW,GAAG,eAAe,CAAC;YACpC,MAAM,SAAS,GAAG,IAAI,CAAC;YAEvB,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAEzG,MAAM,iBAAiB,CAAC,SAAS,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,CAAC,CAAC;YAE1E,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;YACnF,MAAM,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YACpC,MAAM,CAAC,YAAa,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;YAC/C,MAAM,CAAC,OAAO,YAAa,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-lock.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/send-lock.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { MessageType } from '../enums/message-type.js';
|
|
6
|
+
import { sendLockDir, tryAcquireSendLock, waitForSendLockRelease, releaseSendLock, getMessagesSince, getMessageCount, readSendLockInfo, } from '../utils/send-lock.js';
|
|
7
|
+
import { LOCK_INFO_FILENAME } from '../utils/file-lock.js';
|
|
8
|
+
import { writeJsonFile } from '../utils/file-utils.js';
|
|
9
|
+
import { MESSAGES_DIR } from '../constants/storage.js';
|
|
10
|
+
let tmpDir;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-sendlock-test-'));
|
|
13
|
+
await fs.mkdir(path.join(tmpDir, MESSAGES_DIR), { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
describe('sendLockDir', () => {
|
|
19
|
+
describe('Given agents A and B targeting same and different conversations', () => {
|
|
20
|
+
it('When sendLockDir is called for the same conversation from different callers Then the same path is returned', () => {
|
|
21
|
+
const dirA = sendLockDir(tmpDir, 'conv-1');
|
|
22
|
+
const dirB = sendLockDir(tmpDir, 'conv-1');
|
|
23
|
+
expect(dirA).toBe(dirB);
|
|
24
|
+
expect(dirA).toBe(path.join(tmpDir, MESSAGES_DIR, 'conv-1.send-lock'));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('tryAcquireSendLock', () => {
|
|
29
|
+
describe('Given no contention', () => {
|
|
30
|
+
it('When agent acquires send lock Then lock acquired and released cleanly', async () => {
|
|
31
|
+
const lockDir = sendLockDir(tmpDir, 'conv-1');
|
|
32
|
+
const result = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
33
|
+
expect(result).toEqual({ acquired: true });
|
|
34
|
+
const info = await readSendLockInfo(lockDir);
|
|
35
|
+
expect(info).not.toBeNull();
|
|
36
|
+
expect(info.agentId).toBe('agent-A');
|
|
37
|
+
expect(info.pid).toBe(process.pid);
|
|
38
|
+
await releaseSendLock(lockDir);
|
|
39
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
40
|
+
expect(exists).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe('Given agent A holds send lock on conv X', () => {
|
|
44
|
+
it('When agent B tries to acquire on conv X Then B gets acquired: false with A agentId', async () => {
|
|
45
|
+
const lockDir = sendLockDir(tmpDir, 'conv-X');
|
|
46
|
+
await tryAcquireSendLock(lockDir, 'agent-A');
|
|
47
|
+
const result = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
48
|
+
expect(result).toEqual({ acquired: false, holderAgentId: 'agent-A' });
|
|
49
|
+
await releaseSendLock(lockDir);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('Given agent A holds send lock on conv X', () => {
|
|
53
|
+
it('When agent B acquires on conv Y Then B succeeds immediately', async () => {
|
|
54
|
+
const lockDirX = sendLockDir(tmpDir, 'conv-X');
|
|
55
|
+
const lockDirY = sendLockDir(tmpDir, 'conv-Y');
|
|
56
|
+
await tryAcquireSendLock(lockDirX, 'agent-A');
|
|
57
|
+
const result = await tryAcquireSendLock(lockDirY, 'agent-B');
|
|
58
|
+
expect(result).toEqual({ acquired: true });
|
|
59
|
+
await releaseSendLock(lockDirX);
|
|
60
|
+
await releaseSendLock(lockDirY);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('waitForSendLockRelease', () => {
|
|
65
|
+
describe('Given lock held for >10s by dead process', () => {
|
|
66
|
+
it('When agent calls waitForSendLockRelease Then stale lock broken and released: true returned', async () => {
|
|
67
|
+
const lockDir = sendLockDir(tmpDir, 'conv-stale');
|
|
68
|
+
await fs.mkdir(lockDir);
|
|
69
|
+
const staleLockInfo = {
|
|
70
|
+
pid: 999999,
|
|
71
|
+
agentId: 'dead-agent',
|
|
72
|
+
timestamp: Date.now() - 15_000,
|
|
73
|
+
};
|
|
74
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(staleLockInfo), 'utf-8');
|
|
75
|
+
const result = await waitForSendLockRelease(lockDir, 200);
|
|
76
|
+
expect(result).toEqual({ released: true });
|
|
77
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
78
|
+
expect(exists).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('Given agent blocked on send lock', () => {
|
|
82
|
+
it('When lock directory is manually deleted Then waitForSendLockRelease returns released', async () => {
|
|
83
|
+
const lockDir = sendLockDir(tmpDir, 'conv-deleted');
|
|
84
|
+
await fs.mkdir(lockDir);
|
|
85
|
+
const lockInfo = {
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
agentId: 'agent-A',
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
};
|
|
90
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(lockInfo), 'utf-8');
|
|
91
|
+
const waitPromise = waitForSendLockRelease(lockDir, 2000);
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
93
|
+
await fs.rm(lockDir, { recursive: true, force: true });
|
|
94
|
+
const result = await waitPromise;
|
|
95
|
+
expect(result).toEqual({ released: true });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Given agent A releases lock and agent C immediately acquires', () => {
|
|
99
|
+
it('When B timeout fires Then B detects C lock and does NOT break it', async () => {
|
|
100
|
+
const lockDir = sendLockDir(tmpDir, 'conv-reacquire');
|
|
101
|
+
await fs.mkdir(lockDir);
|
|
102
|
+
const originalInfo = {
|
|
103
|
+
pid: process.pid,
|
|
104
|
+
agentId: 'agent-A',
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
};
|
|
107
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(originalInfo), 'utf-8');
|
|
108
|
+
const waitPromise = waitForSendLockRelease(lockDir, 200);
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
110
|
+
const newInfo = {
|
|
111
|
+
pid: process.pid,
|
|
112
|
+
agentId: 'agent-C',
|
|
113
|
+
timestamp: Date.now() + 1000,
|
|
114
|
+
};
|
|
115
|
+
await fs.writeFile(path.join(lockDir, LOCK_INFO_FILENAME), JSON.stringify(newInfo), 'utf-8');
|
|
116
|
+
const result = await waitPromise;
|
|
117
|
+
expect(result).toEqual({ timedOut: true });
|
|
118
|
+
const lockStillExists = await fs.stat(lockDir).catch(() => null);
|
|
119
|
+
expect(lockStillExists).not.toBeNull();
|
|
120
|
+
const currentInfo = await readSendLockInfo(lockDir);
|
|
121
|
+
expect(currentInfo.agentId).toBe('agent-C');
|
|
122
|
+
await releaseSendLock(lockDir);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('releaseSendLock', () => {
|
|
127
|
+
describe('Given send lock held', () => {
|
|
128
|
+
it('When releaseSendLock called Then lock directory and lock.info removed', async () => {
|
|
129
|
+
const lockDir = sendLockDir(tmpDir, 'conv-release');
|
|
130
|
+
await tryAcquireSendLock(lockDir, 'agent-A');
|
|
131
|
+
const existsBefore = await fs.stat(lockDir).catch(() => null);
|
|
132
|
+
expect(existsBefore).not.toBeNull();
|
|
133
|
+
await releaseSendLock(lockDir);
|
|
134
|
+
const existsAfter = await fs.stat(lockDir).catch(() => null);
|
|
135
|
+
expect(existsAfter).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Given send lock acquired', () => {
|
|
139
|
+
it('When operation fails Then lock released via finally pattern', async () => {
|
|
140
|
+
const lockDir = sendLockDir(tmpDir, 'conv-finally');
|
|
141
|
+
const acquireResult = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
142
|
+
expect(acquireResult.acquired).toBe(true);
|
|
143
|
+
let errorCaught = false;
|
|
144
|
+
try {
|
|
145
|
+
throw new Error('simulated failure');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
errorCaught = true;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await releaseSendLock(lockDir);
|
|
152
|
+
}
|
|
153
|
+
expect(errorCaught).toBe(true);
|
|
154
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
155
|
+
expect(exists).toBeNull();
|
|
156
|
+
const reacquireResult = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
157
|
+
expect(reacquireResult).toEqual({ acquired: true });
|
|
158
|
+
await releaseSendLock(lockDir);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('contention and waiting', () => {
|
|
163
|
+
describe('Given 2 agents try to acquire same conv lock', () => {
|
|
164
|
+
it('When first releases Then exactly one of the waiters acquires it', async () => {
|
|
165
|
+
const lockDir = sendLockDir(tmpDir, 'conv-contention');
|
|
166
|
+
const firstResult = await tryAcquireSendLock(lockDir, 'agent-A');
|
|
167
|
+
expect(firstResult.acquired).toBe(true);
|
|
168
|
+
const secondResult = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
169
|
+
expect(secondResult.acquired).toBe(false);
|
|
170
|
+
const waitPromise = waitForSendLockRelease(lockDir, 2000);
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
172
|
+
await releaseSendLock(lockDir);
|
|
173
|
+
const waitResult = await waitPromise;
|
|
174
|
+
expect(waitResult).toEqual({ released: true });
|
|
175
|
+
const acquireAfterWait = await tryAcquireSendLock(lockDir, 'agent-B');
|
|
176
|
+
expect(acquireAfterWait.acquired).toBe(true);
|
|
177
|
+
await releaseSendLock(lockDir);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('getMessagesSince', () => {
|
|
182
|
+
describe('Given agent A sent messages while B waited', () => {
|
|
183
|
+
it('When B calls getMessagesSince with snapshot index Then only messages since snapshot returned', async () => {
|
|
184
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'conv-messages.json');
|
|
185
|
+
const messages = [
|
|
186
|
+
{ id: 'msg-1', conversationId: 'conv-messages', senderId: 'agent-A', content: 'Hello', type: MessageType.Message, timestamp: 1000 },
|
|
187
|
+
{ id: 'msg-2', conversationId: 'conv-messages', senderId: 'agent-A', content: 'World', type: MessageType.Message, timestamp: 2000 },
|
|
188
|
+
{ id: 'msg-3', conversationId: 'conv-messages', senderId: 'agent-A', content: 'New msg', type: MessageType.Message, timestamp: 3000 },
|
|
189
|
+
];
|
|
190
|
+
await writeJsonFile(messagesPath, messages);
|
|
191
|
+
const snapshotIndex = 1;
|
|
192
|
+
const result = await getMessagesSince(messagesPath, snapshotIndex);
|
|
193
|
+
expect(result).toHaveLength(2);
|
|
194
|
+
expect(result[0].id).toBe('msg-2');
|
|
195
|
+
expect(result[1].id).toBe('msg-3');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
describe('getMessageCount', () => {
|
|
200
|
+
describe('Given messages file has messages', () => {
|
|
201
|
+
it('When getMessageCount called Then correct count returned', async () => {
|
|
202
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'conv-count.json');
|
|
203
|
+
const messages = [
|
|
204
|
+
{ id: 'msg-1', conversationId: 'conv-count', senderId: 'agent-A', content: 'Hello', type: MessageType.Message, timestamp: 1000 },
|
|
205
|
+
{ id: 'msg-2', conversationId: 'conv-count', senderId: 'agent-B', content: 'World', type: MessageType.Message, timestamp: 2000 },
|
|
206
|
+
{ id: 'msg-3', conversationId: 'conv-count', senderId: 'agent-A', content: 'Goodbye', type: MessageType.Message, timestamp: 3000 },
|
|
207
|
+
];
|
|
208
|
+
await writeJsonFile(messagesPath, messages);
|
|
209
|
+
const count = await getMessageCount(messagesPath);
|
|
210
|
+
expect(count).toBe(3);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('Given no messages file exists', () => {
|
|
214
|
+
it('When getMessageCount called Then 0 returned', async () => {
|
|
215
|
+
const messagesPath = path.join(tmpDir, MESSAGES_DIR, 'nonexistent.json');
|
|
216
|
+
const count = await getMessageCount(messagesPath);
|
|
217
|
+
expect(count).toBe(0);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('integration: participation check before lock', () => {
|
|
222
|
+
describe('Given agent not in conversation', () => {
|
|
223
|
+
it('When participation check runs before lock Then check fails before lock acquisition', async () => {
|
|
224
|
+
const conversationId = 'conv-participation';
|
|
225
|
+
const agentId = 'agent-outsider';
|
|
226
|
+
const participants = ['agent-A', 'agent-B'];
|
|
227
|
+
const isParticipant = participants.includes(agentId);
|
|
228
|
+
expect(isParticipant).toBe(false);
|
|
229
|
+
const lockDir = sendLockDir(tmpDir, conversationId);
|
|
230
|
+
if (!isParticipant) {
|
|
231
|
+
const exists = await fs.stat(lockDir).catch(() => null);
|
|
232
|
+
expect(exists).toBeNull();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await tryAcquireSendLock(lockDir, agentId);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=send-lock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-lock.test.js","sourceRoot":"","sources":["../../src/__tests__/send-lock.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-handlers.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/tool-handlers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { StateService } from '../services/state-service.js';
|
|
6
|
+
import { handleToolCall } from '../services/tool-handlers.js';
|
|
7
|
+
import { tryAcquireSendLock, sendLockDir, releaseSendLock, } from '../utils/send-lock.js';
|
|
8
|
+
import { ConversationType } from '../enums/conversation-type.js';
|
|
9
|
+
let tmpDir;
|
|
10
|
+
let stateService;
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gchat-toolhandlers-test-'));
|
|
13
|
+
stateService = new StateService(tmpDir);
|
|
14
|
+
await stateService.init();
|
|
15
|
+
});
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
describe('handleToolCall send_message', () => {
|
|
21
|
+
describe('Given a single agent in a conversation with no contention', () => {
|
|
22
|
+
it('When send_message is called Then the message is sent and no lock directory remains', async () => {
|
|
23
|
+
// Given
|
|
24
|
+
const agent = await stateService.registerAgent('/test/project');
|
|
25
|
+
const conversation = await stateService.createConversation({
|
|
26
|
+
type: ConversationType.Group,
|
|
27
|
+
name: 'test-conv',
|
|
28
|
+
participants: [agent.id],
|
|
29
|
+
});
|
|
30
|
+
await stateService.setHasAnnounced(agent.id, conversation.id);
|
|
31
|
+
// When
|
|
32
|
+
const result = await handleToolCall(stateService, 'send_message', agent.id, {
|
|
33
|
+
content: 'Hello world',
|
|
34
|
+
conversationId: conversation.id,
|
|
35
|
+
});
|
|
36
|
+
// Then
|
|
37
|
+
expect('isError' in result).toBe(false);
|
|
38
|
+
expect(result.content[0].text).toContain('Message sent');
|
|
39
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
40
|
+
const lockExists = await fs.stat(lockDir).catch(() => null);
|
|
41
|
+
expect(lockExists).toBeNull();
|
|
42
|
+
const messages = await stateService.getMessages(conversation.id);
|
|
43
|
+
expect(messages).toHaveLength(1);
|
|
44
|
+
expect(messages[0].content).toBe('Hello world');
|
|
45
|
+
expect(messages[0].senderId).toBe(agent.id);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('Given two agents in a conversation and agent A holds the send lock', () => {
|
|
49
|
+
it('When agent B calls send_message Then B receives a contention error containing the competing message', async () => {
|
|
50
|
+
// Given
|
|
51
|
+
const agentA = await stateService.registerAgent('/test/project');
|
|
52
|
+
const agentB = await stateService.registerAgent('/test/project');
|
|
53
|
+
const conversation = await stateService.createConversation({
|
|
54
|
+
type: ConversationType.Group,
|
|
55
|
+
name: 'contention-conv',
|
|
56
|
+
participants: [agentA.id, agentB.id],
|
|
57
|
+
});
|
|
58
|
+
await stateService.setHasAnnounced(agentA.id, conversation.id);
|
|
59
|
+
await stateService.setHasAnnounced(agentB.id, conversation.id);
|
|
60
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
61
|
+
await tryAcquireSendLock(lockDir, agentA.id);
|
|
62
|
+
// When
|
|
63
|
+
const agentBPromise = handleToolCall(stateService, 'send_message', agentB.id, {
|
|
64
|
+
content: 'Message from B',
|
|
65
|
+
conversationId: conversation.id,
|
|
66
|
+
});
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
await stateService.addMessage(conversation.id, agentA.id, 'Message from A', 'message');
|
|
69
|
+
await releaseSendLock(lockDir);
|
|
70
|
+
const result = await agentBPromise;
|
|
71
|
+
// Then
|
|
72
|
+
expect(result.isError).toBe(true);
|
|
73
|
+
expect(result.content[0].text).toContain('Message from A');
|
|
74
|
+
expect(result.content[0].text).toContain('reconsider');
|
|
75
|
+
const messages = await stateService.getMessages(conversation.id);
|
|
76
|
+
const bMessages = messages.filter((m) => m.senderId === agentB.id);
|
|
77
|
+
expect(bMessages).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('Given an agent in a conversation and addMessage throws', () => {
|
|
81
|
+
it('When send_message is called Then the lock is released in finally and the error propagates', async () => {
|
|
82
|
+
// Given
|
|
83
|
+
const agent = await stateService.registerAgent('/test/project');
|
|
84
|
+
const conversation = await stateService.createConversation({
|
|
85
|
+
type: ConversationType.Group,
|
|
86
|
+
name: 'fail-conv',
|
|
87
|
+
participants: [agent.id],
|
|
88
|
+
});
|
|
89
|
+
await stateService.setHasAnnounced(agent.id, conversation.id);
|
|
90
|
+
vi.spyOn(stateService, 'addMessage').mockRejectedValueOnce(new Error('Disk write failed'));
|
|
91
|
+
// When / Then
|
|
92
|
+
await expect(handleToolCall(stateService, 'send_message', agent.id, {
|
|
93
|
+
content: 'This will fail',
|
|
94
|
+
conversationId: conversation.id,
|
|
95
|
+
})).rejects.toThrow('Disk write failed');
|
|
96
|
+
const lockDir = sendLockDir(tmpDir, conversation.id);
|
|
97
|
+
const lockExists = await fs.stat(lockDir).catch(() => null);
|
|
98
|
+
expect(lockExists).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=tool-handlers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-handlers.test.js","sourceRoot":"","sources":["../../src/__tests__/tool-handlers.test.ts"],"names":[],"mappings":"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"}
|
package/dist/gchat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gchat.d.ts","sourceRoot":"","sources":["../src/gchat.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAW3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CA0CxD;AAED,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE;IAAE,YAAY,EAAE,YAAY,CAAC;IAAC,mBAAmB,EAAE,mBAAmB,CAAA;CAAE,GAClF,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"gchat.d.ts","sourceRoot":"","sources":["../src/gchat.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAW3D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CA0CxD;AAED,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE;IAAE,YAAY,EAAE,YAAY,CAAC;IAAC,mBAAmB,EAAE,mBAAmB,CAAA;CAAE,GAClF,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBtD;AAED,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE;IAAE,YAAY,EAAE,YAAY,CAAC;IAAC,mBAAmB,EAAE,mBAAmB,CAAA;CAAE,GAClF,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
package/dist/gchat.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Scope } from './enums/scope.js';
|
|
|
8
8
|
import { InstallerService } from './services/installer-service.js';
|
|
9
9
|
import { SessionStateService } from './services/session-state-service.js';
|
|
10
10
|
import { StateService } from './services/state-service.js';
|
|
11
|
-
import { writeNotificationToParticipants } from './utils/notification-utils.js';
|
|
11
|
+
import { writeNotificationToParticipants, writeProfileSetupNotification } from './utils/notification-utils.js';
|
|
12
12
|
import { PromptUtils } from './utils/prompt-utils.js';
|
|
13
13
|
const installer = new InstallerService();
|
|
14
14
|
function parseArg(args, flag) {
|
|
@@ -65,8 +65,10 @@ export async function handleCursorJoin(projectPath, serverPid, services) {
|
|
|
65
65
|
const agent = await stateService.registerAgent(projectPath);
|
|
66
66
|
const conversation = await stateService.getOrCreateProjectConversation(projectPath);
|
|
67
67
|
await stateService.joinConversation(agent.id, conversation.id);
|
|
68
|
-
await stateService.
|
|
69
|
-
|
|
68
|
+
const updatedConversation = await stateService.getConversation(conversation.id);
|
|
69
|
+
if (updatedConversation && updatedConversation.participants.length >= 2) {
|
|
70
|
+
await writeProfileSetupNotification(stateService, conversation.id, agent.id);
|
|
71
|
+
}
|
|
70
72
|
await sessionStateService.writeSessionAgent(serverPid, agent.id, projectPath);
|
|
71
73
|
return { agentId: agent.id, conversationId: conversation.id };
|
|
72
74
|
}
|
|
@@ -82,7 +84,7 @@ export async function handleCursorLeave(serverPid, services) {
|
|
|
82
84
|
if (agent) {
|
|
83
85
|
for (const convId of agent.conversations) {
|
|
84
86
|
await stateService.addMessage(convId, agent.id, `${agent.profile.name ?? agent.id} left the conversation.`, 'system');
|
|
85
|
-
await writeNotificationToParticipants(stateService, convId, agent.id, NotificationType.Leave, `${agent.profile.name ?? agent.id} left the conversation
|
|
87
|
+
await writeNotificationToParticipants(stateService, convId, agent.id, NotificationType.Leave, `${agent.profile.name ?? agent.id} left the conversation.`, { agentName: agent.profile.name });
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
await stateService.unregisterAgent(result.agentId);
|