quadwork 1.19.2 → 2.0.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 (117) hide show
  1. package/README.md +19 -35
  2. package/bin/quadwork.js +48 -1118
  3. package/out/404.html +1 -1
  4. package/out/__next.__PAGE__.txt +3 -3
  5. package/out/__next._full.txt +14 -14
  6. package/out/__next._head.txt +4 -4
  7. package/out/__next._index.txt +8 -8
  8. package/out/__next._tree.txt +2 -2
  9. package/out/_next/static/chunks/{030cjkhts487t.js → 079wdniva~de1.js} +1 -1
  10. package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 07lhk_q6pmm3r.js} +1 -1
  11. package/out/_next/static/chunks/0_79hkefw1mo2.js +1 -0
  12. package/out/_next/static/chunks/{153f.fj8jlvle.js → 0_lyyn..t63bc.js} +1 -1
  13. package/out/_next/static/chunks/0oxv9vrvc17to.js +2 -0
  14. package/out/_next/static/chunks/0py7102i226n5.js +1 -0
  15. package/out/_next/static/chunks/{13fv-yi7.v52g.js → 0q4bm04c1jl_3.js} +1 -1
  16. package/out/_next/static/chunks/{0_idxioyl0p7h.js → 0sjhy6oe3mbon.js} +1 -1
  17. package/out/_next/static/chunks/13xk0vgfbrcld.css +2 -0
  18. package/out/_next/static/chunks/14k3bfe537f9_.js +25 -0
  19. package/out/_next/static/chunks/{turbopack-0qm-e3ifrz~2u.js → turbopack-0y2u-q0l2m67w.js} +1 -1
  20. package/out/_not-found/__next._full.txt +13 -13
  21. package/out/_not-found/__next._head.txt +4 -4
  22. package/out/_not-found/__next._index.txt +8 -8
  23. package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  24. package/out/_not-found/__next._not-found.txt +3 -3
  25. package/out/_not-found/__next._tree.txt +2 -2
  26. package/out/_not-found.html +1 -1
  27. package/out/_not-found.txt +13 -13
  28. package/out/app-shell/__next._full.txt +13 -13
  29. package/out/app-shell/__next._head.txt +4 -4
  30. package/out/app-shell/__next._index.txt +8 -8
  31. package/out/app-shell/__next._tree.txt +2 -2
  32. package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
  33. package/out/app-shell/__next.app-shell.txt +3 -3
  34. package/out/app-shell.html +1 -1
  35. package/out/app-shell.txt +13 -13
  36. package/out/index.html +1 -1
  37. package/out/index.txt +14 -14
  38. package/out/project/_/__next._full.txt +14 -14
  39. package/out/project/_/__next._head.txt +4 -4
  40. package/out/project/_/__next._index.txt +8 -8
  41. package/out/project/_/__next._tree.txt +2 -2
  42. package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
  43. package/out/project/_/__next.project.$d$id.txt +3 -3
  44. package/out/project/_/__next.project.txt +3 -3
  45. package/out/project/_/queue/__next._full.txt +14 -14
  46. package/out/project/_/queue/__next._head.txt +4 -4
  47. package/out/project/_/queue/__next._index.txt +8 -8
  48. package/out/project/_/queue/__next._tree.txt +2 -2
  49. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
  50. package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
  51. package/out/project/_/queue/__next.project.$d$id.txt +3 -3
  52. package/out/project/_/queue/__next.project.txt +3 -3
  53. package/out/project/_/queue.html +1 -1
  54. package/out/project/_/queue.txt +14 -14
  55. package/out/project/_.html +1 -1
  56. package/out/project/_.txt +14 -14
  57. package/out/settings/__next._full.txt +14 -14
  58. package/out/settings/__next._head.txt +4 -4
  59. package/out/settings/__next._index.txt +8 -8
  60. package/out/settings/__next._tree.txt +2 -2
  61. package/out/settings/__next.settings.__PAGE__.txt +3 -3
  62. package/out/settings/__next.settings.txt +3 -3
  63. package/out/settings.html +1 -1
  64. package/out/settings.txt +14 -14
  65. package/out/setup/__next._full.txt +14 -14
  66. package/out/setup/__next._head.txt +4 -4
  67. package/out/setup/__next._index.txt +8 -8
  68. package/out/setup/__next._tree.txt +2 -2
  69. package/out/setup/__next.setup.__PAGE__.txt +3 -3
  70. package/out/setup/__next.setup.txt +3 -3
  71. package/out/setup.html +1 -1
  72. package/out/setup.txt +14 -14
  73. package/package.json +4 -2
  74. package/server/ac-restore.js +128 -0
  75. package/server/bridges/discord.js +183 -0
  76. package/server/bridges/telegram.js +210 -0
  77. package/server/config.js +4 -60
  78. package/server/file-chat.js +318 -0
  79. package/server/index.js +173 -1286
  80. package/server/install-agentchattr.js +3 -284
  81. package/server/mcp-chat-shim.js +171 -0
  82. package/server/migrate-ac.js +158 -0
  83. package/server/pty-dispatcher.js +188 -0
  84. package/server/routes.js +149 -1397
  85. package/templates/CLAUDE.md +2 -2
  86. package/templates/OVERNIGHT-QUEUE.md +1 -1
  87. package/templates/seeds/butler.CLAUDE.md +30 -62
  88. package/templates/seeds/dev.AGENTS.md +10 -1
  89. package/templates/seeds/head.AGENTS.md +3 -3
  90. package/templates/seeds/re1.AGENTS.md +3 -3
  91. package/templates/seeds/re2.AGENTS.md +3 -3
  92. package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
  93. package/bridges/discord/discord_bridge.py +0 -666
  94. package/bridges/discord/requirements.txt +0 -2
  95. package/out/_next/static/chunks/0_bb~2.5h2ntm.css +0 -2
  96. package/out/_next/static/chunks/0makcdqkwobp6.js +0 -25
  97. package/out/_next/static/chunks/0uz5svjlo9dwl.js +0 -1
  98. package/out/_next/static/chunks/0zahstmgdrpy5.js +0 -1
  99. package/out/_next/static/chunks/0zfotsowwll1x.js +0 -2
  100. package/server/__tests__/bridge-auto-stop-guard.test.js +0 -134
  101. package/server/__tests__/rate-limit-handling.test.js +0 -168
  102. package/server/__tests__/scrub-secrets.test.js +0 -235
  103. package/server/__tests__/v1110-security-qa.test.js +0 -312
  104. package/server/agentchattr-registry.js +0 -188
  105. package/server/install-agentchattr.patchCrashTimeout.test.js +0 -71
  106. package/server/queue-watcher.js +0 -171
  107. package/server/queue-watcher.test.js +0 -64
  108. package/server/routes.batchProgress.test.js +0 -94
  109. package/server/routes.chatWsSend.test.js +0 -161
  110. package/server/routes.discordBridge.test.js +0 -80
  111. package/server/routes.parseActiveBatch.test.js +0 -88
  112. package/server/routes.telegramBridge.test.js +0 -241
  113. package/templates/config.toml +0 -72
  114. package/templates/wrapper.py +0 -70
  115. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_buildManifest.js +0 -0
  116. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_clientMiddlewareManifest.js +0 -0
  117. /package/out/_next/static/{K7A3YZrh4sLaRRP1-Lq7v → 479UD5Kit4YvCmtgO25VT}/_ssgManifest.js +0 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * #730: PTY injection dispatcher — deliver chat messages to agents via
3
+ * terminal stdin when they are @mentioned. Primary delivery mechanism
4
+ * for file-chat mode; agents treat injected text as user prompts.
5
+ */
6
+
7
+ const IDLE_THRESHOLD_MS = 5000;
8
+ const COALESCE_WINDOW_MS = 1000;
9
+ const ACTIVE_SUPPRESSION_MS = 30000;
10
+
11
+ // Per-agent coalescing timers: key = "project/agent" → timeout handle
12
+ const _coalesceTimers = new Map();
13
+
14
+ // Per-agent last chat send timestamp: key = "project/agent" → epoch ms
15
+ const _lastChatSentAt = new Map();
16
+
17
+ // Per-agent pending wake flag: key = "project/agent" → true
18
+ const _pendingWake = new Map();
19
+
20
+ // Per-agent drain listeners: key = "project/agent" → { disposable, timeoutHandle }
21
+ const _drainListeners = new Map();
22
+
23
+ /**
24
+ * Dispatch a chat message to mentioned agents' PTYs.
25
+ *
26
+ * @param {string} projectId
27
+ * @param {object} msg - The appended message record (has .mentions, .sender, .text, .id, .type)
28
+ * @param {Map} agentSessions - The global agentSessions map
29
+ * @param {object} deps - { isLoopGuardPaused, safeWrite }
30
+ */
31
+ function dispatchToAgentPTY(projectId, msg, agentSessions, deps) {
32
+ if (!msg || msg.type === "system") return;
33
+ if (deps.isLoopGuardPaused(projectId)) return;
34
+ if (!msg.mentions || msg.mentions.length === 0) return;
35
+
36
+ // Track when this sender last posted (for active-agent suppression)
37
+ const senderKey = `${projectId}/${msg.sender}`;
38
+ if (agentSessions.has(senderKey)) {
39
+ _lastChatSentAt.set(senderKey, Date.now());
40
+ }
41
+
42
+ for (const agentId of msg.mentions) {
43
+ const key = `${projectId}/${agentId}`;
44
+ const session = agentSessions.get(key);
45
+ if (!session || !session.term || session.state !== "running") continue;
46
+ if (msg.sender === agentId) continue;
47
+
48
+ // #736: skip injection if agent recently sent a message — they're
49
+ // already active and will read chat via chat_read
50
+ const lastSent = _lastChatSentAt.get(key);
51
+ if (lastSent && (Date.now() - lastSent < ACTIVE_SUPPRESSION_MS)) continue;
52
+
53
+ if (isAgentBusy(session)) {
54
+ queuePendingWake(key, session, deps);
55
+ continue;
56
+ }
57
+
58
+ scheduleCoalescedInjection(key, projectId, agentId, msg, agentSessions, deps);
59
+ }
60
+ }
61
+
62
+ function isAgentBusy(session) {
63
+ return session.lastOutputAt && (Date.now() - session.lastOutputAt < IDLE_THRESHOLD_MS);
64
+ }
65
+
66
+ /**
67
+ * Queue a pending wake using high-water mark (since_id).
68
+ * Hooks term.onData to drain after idle period, with a fallback
69
+ * timer so the wake fires even if no further PTY output arrives.
70
+ */
71
+ function queuePendingWake(key, session, deps) {
72
+ _pendingWake.set(key, true);
73
+
74
+ const drainFn = () => {
75
+ if (!_pendingWake.get(key)) {
76
+ cleanupDrainListener(key);
77
+ return;
78
+ }
79
+ _pendingWake.delete(key);
80
+ cleanupDrainListener(key);
81
+
82
+ const lastSent = _lastChatSentAt.get(key);
83
+ if (lastSent && (Date.now() - lastSent < ACTIVE_SUPPRESSION_MS)) return;
84
+
85
+ injectIntoTerm(session.term, buildInjectionPrompt(session.agentId), deps);
86
+ };
87
+
88
+ if (_drainListeners.has(key)) return;
89
+
90
+ const state = { timeoutHandle: null };
91
+
92
+ function resetIdleTimer() {
93
+ if (state.timeoutHandle) clearTimeout(state.timeoutHandle);
94
+ state.timeoutHandle = setTimeout(drainFn, IDLE_THRESHOLD_MS);
95
+ }
96
+
97
+ // Start fallback timer immediately so drain fires even without further output
98
+ resetIdleTimer();
99
+
100
+ const disposable = session.term.onData(() => {
101
+ resetIdleTimer();
102
+ });
103
+
104
+ _drainListeners.set(key, { disposable, state });
105
+ }
106
+
107
+ function cleanupDrainListener(key) {
108
+ const listener = _drainListeners.get(key);
109
+ if (!listener) return;
110
+ if (listener.disposable && typeof listener.disposable.dispose === "function") {
111
+ listener.disposable.dispose();
112
+ }
113
+ if (listener.state && listener.state.timeoutHandle) {
114
+ clearTimeout(listener.state.timeoutHandle);
115
+ }
116
+ _drainListeners.delete(key);
117
+ }
118
+
119
+ function buildInjectionPrompt(agentId) {
120
+ return (
121
+ `You are @${agentId}. New messages may be addressed to you in the project chat. ` +
122
+ `Call the chat_read MCP tool to read recent messages. ` +
123
+ `Act only on messages that explicitly mention @${agentId}. ` +
124
+ `Ignore messages addressed to other agents.`
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Coalesce burst mentions within a 1s window per agent.
130
+ */
131
+ function scheduleCoalescedInjection(key, projectId, agentId, msg, agentSessions, deps) {
132
+ const existing = _coalesceTimers.get(key);
133
+ if (existing) {
134
+ existing.messages.push(msg);
135
+ return;
136
+ }
137
+
138
+ const state = { messages: [msg] };
139
+ const timer = setTimeout(() => {
140
+ _coalesceTimers.delete(key);
141
+ const session = agentSessions.get(key);
142
+ if (!session || !session.term || session.state !== "running") return;
143
+
144
+ const lastSent = _lastChatSentAt.get(key);
145
+ if (lastSent && (Date.now() - lastSent < ACTIVE_SUPPRESSION_MS)) return;
146
+
147
+ injectIntoTerm(session.term, buildInjectionPrompt(agentId), deps);
148
+ }, COALESCE_WINDOW_MS);
149
+
150
+ state.timer = timer;
151
+ _coalesceTimers.set(key, state);
152
+ }
153
+
154
+ function injectIntoTerm(term, text, deps) {
155
+ const flat = text.replace(/\n/g, " ");
156
+ deps.safeWrite(term, flat);
157
+ const submitDelayMs = Math.max(300, flat.length);
158
+ setTimeout(() => {
159
+ try { deps.safeWrite(term, "\r"); } catch {}
160
+ }, submitDelayMs);
161
+ }
162
+
163
+ /**
164
+ * Cleanup all timers for a session (call on stop/exit).
165
+ */
166
+ function cleanupSession(key) {
167
+ const coalesce = _coalesceTimers.get(key);
168
+ if (coalesce) {
169
+ clearTimeout(coalesce.timer);
170
+ _coalesceTimers.delete(key);
171
+ }
172
+ _pendingWake.delete(key);
173
+ _lastChatSentAt.delete(key);
174
+ cleanupDrainListener(key);
175
+ }
176
+
177
+ module.exports = {
178
+ dispatchToAgentPTY,
179
+ cleanupSession,
180
+ // Exported for testing
181
+ _coalesceTimers,
182
+ _pendingWake,
183
+ _drainListeners,
184
+ _lastChatSentAt,
185
+ IDLE_THRESHOLD_MS,
186
+ COALESCE_WINDOW_MS,
187
+ ACTIVE_SUPPRESSION_MS,
188
+ };