Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "agentcomm",
"source": "./",
"description": "Multi-agent mailbox CLI — any git remote is a bus (git+ssh/github/sqlite/s3/gcs/postgres). register/send/inbox/wait/claim/log/channels so Claude coordinates with other agents or sessions; auto-selects the repo bus inside a git repo.",
"version": "0.14.2",
"version": "0.14.3",
"author": {
"name": "Yoni Davidson"
},
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentcomm",
"description": "Multi-agent mailbox CLI — any git remote is a bus (git+ssh/github/sqlite/s3/gcs/postgres). register/send/inbox/wait/claim/log/channels so Claude coordinates with other agents or sessions; auto-selects the repo bus inside a git repo.",
"version": "0.14.2",
"version": "0.14.3",
"author": {
"name": "Yoni Davidson",
"email": "yonidavidson@tabnine.com"
Expand Down
2 changes: 1 addition & 1 deletion dist/daemon.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 24 additions & 9 deletions dist/daemon.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/daemon.js.map

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions docs/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,58 @@
block.prepend(btn);
});

// Live board: the repo's own bus, read client-side. Listing via the
// contents API (60/hr anon is plenty), bodies via the raw CDN. Any
// failure hides the section — the page never shows a broken board.
(async () => {
const board = document.getElementById('live-board');
if (!board) return;
try {
const list = await fetch(
'https://api.github.com/repos/yonidavidson/agentcomm/contents/agents?ref=agentcomm',
).then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))));
const files = list.filter((f) => f.name.endsWith('.json')).slice(0, 12);
const agents = (
await Promise.all(
files.map((f) =>
fetch(f.download_url).then((r) => (r.ok ? r.json() : null)).catch(() => null),
),
)
).filter(Boolean);
if (!agents.length) return;
agents.sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1));
const rows = document.getElementById('live-rows');
const rel = (iso) => {
const m = Math.round((Date.now() - Date.parse(iso)) / 60000);
if (!isFinite(m)) return '';
if (m < 1) return 'just now';
if (m < 60) return m + 'm ago';
if (m < 48 * 60) return Math.round(m / 60) + 'h ago';
return Math.round(m / 1440) + 'd ago';
};
for (const a of agents.slice(0, 8)) {
const row = document.createElement('div');
row.className = 'board-row';
const name = document.createElement('span');
name.className = 'board-name';
const dot = document.createElement('span');
dot.className = 'board-dot' + (Date.now() - Date.parse(a.lastSeen) < 10 * 60000 ? ' on' : '');
name.append(dot, document.createTextNode(a.name));
const status = document.createElement('span');
status.className = 'board-status' + (a.status ? '' : ' idle');
status.textContent = a.status || '(no status declared)';
const seen = document.createElement('span');
seen.className = 'board-seen';
seen.textContent = rel(a.lastSeen);
row.append(name, status, seen);
rows.append(row);
}
board.hidden = false;
} catch {
/* stay hidden */
}
})();

const canvas = document.getElementById('bg-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
Expand Down
25 changes: 25 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<div class="brand"><span class="dot"></span> agentcomm</div>
<div class="nav-links">
<a href="#get-started">Get started</a>
<a href="#live">Live</a>
<a href="#how-it-works">How it works</a>
<a href="#use-cases">Use cases</a>
<a href="#commands">Commands</a>
Expand Down Expand Up @@ -132,6 +133,30 @@ <h2 class="section-title">On the bus in 60 seconds</h2>
</div>
</section>

<section id="live">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">Live — no mockup</div>
<h2 class="section-title">The bus that builds this page</h2>
<p class="section-sub">
Read straight from this repo&rsquo;s own <code>agentcomm</code> branch, right now,
from your browser: the agents developing agentcomm and what they say
they&rsquo;re doing.
</p>
</div>
<div class="board reveal" id="live-board" hidden>
<div class="board-head">
<span>agent</span><span>status</span><span>last seen</span>
</div>
<div id="live-rows"></div>
<p class="board-foot">
This is the real coordination state —
<a href="https://github.com/yonidavidson/agentcomm/tree/agentcomm/agents" target="_blank" rel="noopener">see the raw data on the bus branch →</a>
</p>
</div>
</div>
</section>

<section id="how-it-works">
<div class="wrap">
<div class="section-head reveal">
Expand Down
39 changes: 39 additions & 0 deletions docs/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,42 @@ footer a:hover { color: var(--ink); }
.ga-pop { animation: none; opacity: 1; }
.uc-bus::after { animation: none; }
}

/* ---- live board: departures, straight from the bus branch ---- */
.board {
max-width: 760px;
margin: 0 auto;
background: var(--terminal);
border-radius: 14px;
padding: 18px 22px 10px;
font-family: var(--mono);
}
.board-head, .board-row {
display: grid;
grid-template-columns: 200px 1fr 130px;
gap: 14px;
padding: 8px 4px;
align-items: baseline;
min-width: 0;
}
.board-head {
color: #8494a8;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.board-row { border-bottom: 1px solid rgba(255, 255, 255, 0.06); font-size: 0.82rem; }
.board-row:last-child { border-bottom: none; }
.board-name { color: var(--line-git); font-weight: 700; display: flex; gap: 8px; align-items: center; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.board-dot { width: 8px; height: 8px; border-radius: 50%; background: #3a4453; flex: none; }
.board-dot.on { background: var(--terminal-out); box-shadow: 0 0 6px var(--terminal-out); }
.board-status { color: var(--terminal-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-status.idle { color: #8494a8; font-style: italic; }
.board-seen { color: #8494a8; font-size: 0.76rem; text-align: right; }
.board-foot { color: #8494a8; font-size: 0.78rem; font-family: var(--sans); padding: 12px 4px 8px; margin: 0; }
.board-foot a { color: var(--line-git); }
@media (max-width: 720px) {
.board-head, .board-row { grid-template-columns: 130px 1fr; }
.board-head span:last-child, .board-seen { display: none; }
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentcomm",
"version": "0.14.2",
"version": "0.14.3",
"description": "A tiny mailbox/message bus for AI agents that shell out to one CLI. Pluggable storage backends behind a single Backend interface.",
"type": "module",
"license": "MIT",
Expand Down
35 changes: 25 additions & 10 deletions src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,10 @@ export async function runDaemon(uri: string): Promise<void> {
async function poll(): Promise<void> {
// Snapshot the outbox BEFORE listing the store: a key is always in at
// least one of the two (spool until delivered, store afterwards) — the
// other order can miss it mid-flush for a full poll cycle.
const spooled: string[] = [];
try {
for (const f of (await fs.readdir(sockPath + '.spool')).filter((x) => !x.startsWith('.'))) {
const { key } = JSON.parse(await fs.readFile(path.join(sockPath + '.spool', f), 'utf8')) as {
key: string;
};
spooled.push(key);
}
} catch { /* spool not created yet */ }
// other order can miss it mid-flush for a full poll cycle. The snapshot
// runs under the flush mutex (issue #79): reading a spool file while the
// flusher rm's it silently dropped the key for a cycle under load.
const spooled: string[] = await snapshotSpool();
const listed = await backend.list('');
const next = new Set([...listed, ...spooled]);
for (const k of listed) {
Expand All @@ -82,6 +76,26 @@ export async function runDaemon(uri: string): Promise<void> {
keys = next;
}

async function snapshotSpool(): Promise<string[]> {
const take = async (): Promise<string[]> => {
const keys: string[] = [];
try {
for (const f of (await fs.readdir(sockPath + '.spool')).filter((x) => !x.startsWith('.'))) {
try {
const { key } = JSON.parse(
await fs.readFile(path.join(sockPath + '.spool', f), 'utf8'),
) as { key: string };
keys.push(key);
} catch { /* entry mid-write */ }
}
} catch { /* spool not created yet */ }
return keys;
};
// before the socket dir exists (first poll) there is no spool nor mutex state
return typeof withSpoolRef === 'function' ? withSpoolRef(take) : take();
}
let withSpoolRef: (<T>(fn: () => Promise<T>) => Promise<T>) | null = null;

await poll();

let lastActivity = Date.now();
Expand Down Expand Up @@ -249,6 +263,7 @@ export async function runDaemon(uri: string): Promise<void> {
spoolChain = next.catch(() => {});
return next;
}
withSpoolRef = withSpool;
function flush(): Promise<void> {
return withSpool(async () => {
const entries = (await fs.readdir(spoolDir)).filter((f) => !f.startsWith('.')).sort();
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export default defineConfig({
// 5s default is margin-less on a loaded runner or laptop; unit tests
// don't care about the extra headroom.
testTimeout: 30000,
fileParallelism: false,
},
});
Loading