Un environnement de développement agentique.
Open source, et il fonctionne avec les modèles que vous utilisez déjà. Lancez un agent sur une machine, puis reprenez la même session depuis une autre.
J'ai lancé 100 envois sur une connexion qui perd une requête sur cinq. Les envois échoués sont passés de 23 à 1.
Le seul échec était un fichier refusé par le serveur, donc une nouvelle tentative ne pouvait rien y faire.
La vitesse d'envoi est maintenant dans l'en-tête à côté du nom du fichier, et elle disparaît quand la file est vide.
Je m'en occupe. J'écrirai ici dès qu'il y aura quelque chose à voir.
Reproduit, et la cause est trouvée. Le correctif est prêt pour la revue.
J'ai ajouté un backoff exponentiel à la file d'envoi. Un fragment en échec est relancé jusqu'à cinq fois, et la barre de progression affiche la tentative. Les tests couvrent le timeout et le cas 503.
Avant, un fragment en échec était abandonné. Désormais la file demande un délai à la politique de backoff et planifie la relance, donc toutes les relances passent par un seul endroit.
Après cinq essais ratés, le fragment échoue pour de bon et l'envoi s'arrête avec une erreur claire, pour qu'une connexion morte ne réessaie pas sans fin.
La modification de la file est dans src/upload/queue.ts, et src/upload/backoff.ts contient les délais.
La file écrit l'état de chaque fragment sur le disque. Je branche maintenant la reprise au redémarrage.
Au démarrage, la file lit l'état enregistré et n'envoie que les fragments inachevés, donc un fichier de 2 Go ne repart pas de zéro.
La stratégie dit d'aller sur le marché, donc j'ai ajouté une fonction pour ça.
1export async function goToMarket() {2await ship();3await tellDenis();4}
Un test passe. Le marché n'a pas encore répondu.
Le test lisait le fichier avant que le watcher ne l'ait écrit. Il attend maintenant l'événement de flush, et 200 exécutions de suite passent.
1test('reads the synced file', async () => {2await writeFile(path, 'hello');3await watcher.flushed(path);4const text = await readFile(path);5expect(text).toBe('hello');6});
L'attente est dans le helper de test, donc les autres tests de sync en profitent aussi.
La nouvelle page est prête. Je dois lancer la migration des réglages avant de retirer les onglets.
| Ancien onglet | Nouvelle section |
|---|---|
| Général | Espace de travail |
| Apparence | Espace de travail |
| Comptes | Profil |
La migration a traité 212 réglages sans erreur et les anciens onglets ont disparu. Tous les réglages sont sur une seule page.
Je m'arrête ici. Les anciens onglets restent jusqu'à ce que vous lanciez la migration.
Deux problèmes. Le cookie de session n'expire jamais, et la redirection après connexion accepte n'importe quelle URL. Le reste est correct.
C'est fait. Les correctifs choisis sont en place, chacun avec un test, et ce que vous avez reporté reste en commentaire sur la pull request.
Compris. Rien n'a changé, et les constats restent en commentaires sur la pull request.
J'ai essayé la nouvelle commande sur une machine vierge et elle fonctionne.
1bun add -g uploader2uploader --help
L'ancienne commande curl fonctionne encore aussi, elle peut donc rester en solution de repli.
C'est fait. La nouvelle commande est là où vous l'avez demandée, et l'ancienne reste en solution de repli.
L'essentiel du temps sert à relire tous les fichiers à chaque recherche. Un index le réduit, et les petits dépôts changent à peine.
L'index se met à jour quand un fichier change, donc il n'a jamais besoin d'une reconstruction complète.
L'arborescence se charge maintenant depuis un cache sur le disque et se rafraîchit en arrière-plan. Seule l'attente change.
- Accès au cache
- 94%
- Taille sur le disque
- 2.1 MB
- Chargement de l'arborescence
- 40 ms
Le cache se vide tout seul quand la branche change.
Trouvé. Un fichier vide donne null, et le code y lit un champ tout de suite.
1export function loadConfig(path: string) {2const text = readFileSync(path, 'utf8');3if (text.trim() === '') return defaults;4return { ...defaults, ...parse(text) };5}
Maintenant un fichier vide vaut la config par défaut, et un fichier cassé affiche une erreur claire.
Cinq nouvelles issues. Trois sont de vrais bugs, et l'un d'eux peut attendre la prochaine version.
| Issue | Type |
|---|---|
| Upload stalls at 99% on Safari | bug |
| Crash on a file named final_final_v2_🦆.mov | bug |
| Progress bar jumps back after resume | bug |
| Drag a folder into the upload list | feature |
| Is there a size limit? Asking for a 400 GB log file | question |
Je les ai toutes étiquetées et j'ai lié le doublon au rapport plus ancien.
Sur quoi travaillons-nous dans uploader ?
Relancer les envois échoués
J'ai ajouté un backoff exponentiel à la file d'envoi. Un fragment en échec est relancé jusqu'à cinq fois, et la barre de progression affiche la tentative. Les tests couvrent le timeout et le cas 503.
Avant, un fragment en échec était abandonné. Désormais la file demande un délai à la politique de backoff et planifie la relance, donc toutes les relances passent par un seul endroit.
const result = await sendChunk(chunk); if (!result.ok) return fail(chunk); if (!result.ok) { const delay = backoff.next(chunk.attempt); if (delay === null) return fail(chunk); return schedule(chunk, delay); } progress.advance(chunk.size); const DELAYS = [500, 1000, 2000, 4000, 8000]; export const backoff = { next(attempt: number) { return DELAYS[attempt] ?? null; } }; 1import { backoff } from './backoff';2import { sendChunk } from './client';3import { progress } from './progress';4import type { Chunk, UploadResult } from './types';56// TODO: remove before launch (added in 2023)7const MAX_PARALLEL = 4;8const queue: Chunk[] = [];9let running = 0;1011export function enqueue(chunks: Chunk[]) {12queue.push(...chunks);13void drain();14}1516async function drain() {17while (running < MAX_PARALLEL && queue.length > 0) {18const chunk = queue.shift();19if (!chunk) return;20running += 1;21await upload(chunk);22running -= 1;23}24}2526export async function upload(chunk: Chunk): Promise<UploadResult> {27const result = await sendChunk(chunk);28if (!result.ok) {29const delay = backoff.next(chunk.attempt);30if (delay === null) return fail(chunk);31return schedule(chunk, delay);32}33progress.advance(chunk.size);34return done(chunk);35}3637function schedule(chunk: Chunk, delay: number) {38chunk.attempt += 1;39progress.retrying(chunk);40setTimeout(() => enqueue([chunk]), delay);41}
Tourne sur votre machine. Accessible de partout.
L'application de bureau exécute les agents sur votre ordinateur et se connecte à vos serveurs. Associez votre téléphone une fois avec un QR code, puis il les rejoint via Private Relay, chiffré de bout en bout et sans port ouvert.
Sur quoi travaillons-nous dans uploader ?
Sur quoi travaillons-nous dans uploader ?
Sur quoi travaillons-nous dans uploader ?
Aucune session trouvée.
const result = await sendChunk(chunk); if (!result.ok) { const delay = backoff.next(chunk.attempt); if (delay === null) return fail(chunk); return schedule(chunk, delay); }
const DELAYS = [500, 1000, 2000, 4000, 8000]; export const backoff = { next(attempt: number) { return DELAYS[attempt] ?? null; } };
test('retries a chunk after a 503', async () => { server.respondWith(503); await upload(chunk); clock.tick(500); expect(server.calls).toBe(2); });
Une tâche, plusieurs modèles.
Envoyez le même prompt à plusieurs modèles à la fois. Chaque exécution peut avoir son propre worktree et sa propre branche. Gardez le résultat qui vous plaît le plus ou fusionnez les réponses en une seule.
Lisez un gros diff comme une visite guidée.
Le walkthrough remet un gros diff dans l'ordre où il a du sens, explique chaque étape en une phrase et signale le changement clé, pour que vous sachiez quoi lire en premier.
Vos tickets et vos outils, à un clic.
Lancez une session depuis une issue GitHub, une pull request ou un ticket Linear, et l'agent reçoit tout le fil. Le catalogue d'intégrations compte plus de 160 services, dont Figma et Sentry, et chacun s'installe en un clic.
Donnez-lui un objectif ou un horaire.
Fixez un objectif et l'agent continue, tour après tour, jusqu'à ce qu'une vérification séparée dise que c'est fini ou bloqué. Les tâches planifiées lancent une session toutes seules, chaque matin ou une seule fois mardi prochain.
Bureau, téléphone, navigateur, éditeur.
De la part de ceux qui l’utilisent.
Des messages que nous n’avons pas demandés.
I've had a deep experience with openchamber. I think my choice was right. This thing is really useful, smooth and silky. Go try it out quick, I strongly recommend it, no more messing around blindly.
@realchendahuang Was looking for a way to run agents from my phone, remotely on my server, other than tmux. Found OpenChamber. It is INSANELY good, highly recommended.
@NihilSineTheo Seriously, I've spent almost the whole week trying to find a tool that made sense to orchestrate agents and, as a dev (actually, a devops), openchamber is the only one that felt natural and logical.
@1un4rPunk I just want to say the app itself is phenomenal. The level of polish is just unreal. I saw OpenChamber back when it was a small project, I think maybe a couple months ago, and now it is unrecognizable. Truly amazing work. The latest release with its streaming feature is everything I could have possibly asked for and more. It is gorgeous and smooth.
Installez-le sur la machine que vous avez déjà.
L’application desktop sur macOS, Windows ou Linux, ou une commande sur un serveur. Ensuite, ouvrez-le d’où vous êtes.
OpenChamber ne collecte rien sur votre travail. Le code est sous licence MIT, donc chacun peut vérifier.
Activité
Les délais peuvent-ils aller dans la config ? On en voudra peut-être de plus longs sur mobile.
Bonne idée. Les délais sont un seul tableau, ça peut donc faire une PR suivante.