@loicngr/kobo 1.10.0 → 1.10.1
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
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.10.1
|
|
8
|
+
|
|
9
|
+
- fix(auto-loop): recover from interrupted sessions (#18)
|
|
10
|
+
|
|
7
11
|
## 1.10.0
|
|
8
12
|
|
|
9
13
|
- fix(agent): make Claude sessions recover cleanly from a stuck post-result
|
package/dist/server/index.js
CHANGED
|
@@ -388,6 +388,9 @@ setMessageHandler(async (type, payload) => {
|
|
|
388
388
|
}
|
|
389
389
|
if (type === 'workspace:stop' && p?.workspaceId) {
|
|
390
390
|
try {
|
|
391
|
+
// A deliberate stop must not be treated like a recoverable engine
|
|
392
|
+
// interruption: disable before the engine emits session:ended(killed).
|
|
393
|
+
autoLoopService.disable(p.workspaceId, 'user-action');
|
|
391
394
|
stopAgent(p.workspaceId);
|
|
392
395
|
}
|
|
393
396
|
catch (err) {
|
|
@@ -395,11 +395,12 @@ function reuseOrCreateFreshSession(workspaceId, existingSessionId, model) {
|
|
|
395
395
|
}
|
|
396
396
|
// ── Event handler ─────────────────────────────────────────────────────────────
|
|
397
397
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
* `
|
|
398
|
+
* Snapshots of task completion and task state at `session:started`, read back
|
|
399
|
+
* at `session:ended` for auto-loop stall detection. A task moved to
|
|
400
|
+
* `in_progress` is meaningful progress even if it is not done yet.
|
|
401
401
|
*/
|
|
402
402
|
const tasksDoneSnapshot = new Map();
|
|
403
|
+
const taskStateSnapshot = new Map();
|
|
403
404
|
function getDoneTaskCount(workspaceId) {
|
|
404
405
|
try {
|
|
405
406
|
const db = getDb();
|
|
@@ -415,9 +416,23 @@ function getDoneTaskCount(workspaceId) {
|
|
|
415
416
|
return 0;
|
|
416
417
|
}
|
|
417
418
|
}
|
|
419
|
+
function getTaskStateSignature(workspaceId) {
|
|
420
|
+
try {
|
|
421
|
+
const db = getDb();
|
|
422
|
+
const rows = db
|
|
423
|
+
.prepare('SELECT id, status, updated_at FROM tasks WHERE workspace_id = ? ORDER BY id')
|
|
424
|
+
.all(workspaceId);
|
|
425
|
+
return JSON.stringify(rows);
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
console.warn('[orchestrator] getTaskStateSignature failed, returning empty state:', err);
|
|
429
|
+
return '';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
418
432
|
/** Clear the in-memory done-count snapshot for a workspace (called on delete). */
|
|
419
433
|
export function forgetTasksDoneSnapshot(workspaceId) {
|
|
420
434
|
tasksDoneSnapshot.delete(workspaceId);
|
|
435
|
+
taskStateSnapshot.delete(workspaceId);
|
|
421
436
|
}
|
|
422
437
|
/** Drop the resume-failed flag for a workspace (called on delete). */
|
|
423
438
|
export function forgetResumeFailed(workspaceId) {
|
|
@@ -444,6 +459,7 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
444
459
|
// can compute a delta for auto-loop stall detection.
|
|
445
460
|
if (ev.kind === 'session:started') {
|
|
446
461
|
tasksDoneSnapshot.set(workspaceId, getDoneTaskCount(workspaceId));
|
|
462
|
+
taskStateSnapshot.set(workspaceId, getTaskStateSignature(workspaceId));
|
|
447
463
|
}
|
|
448
464
|
// Legacy fallback: the built-in `ScheduleWakeup` tool (CLI tradition) isn't
|
|
449
465
|
// declared by the SDK, so we intercept the tool:call event and apply the
|
|
@@ -531,8 +547,12 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
531
547
|
const isResumeFailed = resumeFailedSet.delete(workspaceId);
|
|
532
548
|
const before = tasksDoneSnapshot.get(workspaceId) ?? getDoneTaskCount(workspaceId);
|
|
533
549
|
const after = getDoneTaskCount(workspaceId);
|
|
534
|
-
const
|
|
550
|
+
const completedDelta = Math.max(0, after - before);
|
|
551
|
+
const taskStateBefore = taskStateSnapshot.get(workspaceId) ?? getTaskStateSignature(workspaceId);
|
|
552
|
+
const taskStateAfter = getTaskStateSignature(workspaceId);
|
|
553
|
+
const progressDelta = completedDelta > 0 || taskStateBefore !== taskStateAfter ? 1 : 0;
|
|
535
554
|
tasksDoneSnapshot.delete(workspaceId);
|
|
555
|
+
taskStateSnapshot.delete(workspaceId);
|
|
536
556
|
clearPendingForSession(workspaceId, agentSessionId);
|
|
537
557
|
// Must run BEFORE autoLoopService.onSessionEnded → spawnNextIteration →
|
|
538
558
|
// startAgent, otherwise startAgent throws "Agent already running" because
|
|
@@ -546,7 +566,7 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
546
566
|
// disable() clears it, and the cleanup hook needs to know whether this was
|
|
547
567
|
// a mid-loop session (never cleans) or a standalone one.
|
|
548
568
|
const wasAutoLoop = autoLoopService.getStatus(workspaceId).auto_loop;
|
|
549
|
-
autoLoopService.onSessionEnded(workspaceId, effectiveReason,
|
|
569
|
+
autoLoopService.onSessionEnded(workspaceId, effectiveReason, progressDelta);
|
|
550
570
|
cleanupScriptService.onSessionEnded(workspaceId, effectiveReason, { wasAutoLoop });
|
|
551
571
|
}
|
|
552
572
|
if (ev.kind === 'session:user-input-requested') {
|
|
@@ -82,7 +82,15 @@ export function disable(workspaceId, reason) {
|
|
|
82
82
|
return;
|
|
83
83
|
const db = getDb();
|
|
84
84
|
db.prepare('UPDATE workspaces SET auto_loop = 0 WHERE id = ?').run(workspaceId);
|
|
85
|
-
|
|
85
|
+
const diagnostic = {
|
|
86
|
+
reason,
|
|
87
|
+
tasksPending: countPendingTasks(workspaceId),
|
|
88
|
+
noProgressStreak: row.no_progress_streak,
|
|
89
|
+
status: row.status,
|
|
90
|
+
};
|
|
91
|
+
console.info(`[auto-loop] disabled workspace '${workspaceId}' reason=${reason}` +
|
|
92
|
+
` pending=${diagnostic.tasksPending} streak=${diagnostic.noProgressStreak} status=${diagnostic.status}`);
|
|
93
|
+
emitEphemeral(workspaceId, 'autoloop:disabled', diagnostic);
|
|
86
94
|
// The loop finished every task — run the project's cleanup script. Other
|
|
87
95
|
// disable reasons (stall / error / user-action) leave tasks unfinished, so
|
|
88
96
|
// they intentionally skip the cleanup.
|
|
@@ -93,15 +101,15 @@ export function disable(workspaceId, reason) {
|
|
|
93
101
|
/**
|
|
94
102
|
* Route a `session:ended` event into the auto-loop state machine.
|
|
95
103
|
*
|
|
96
|
-
* Called by orchestrator.handleEvent. The delta
|
|
97
|
-
*
|
|
98
|
-
*
|
|
104
|
+
* Called by orchestrator.handleEvent. The delta records whether task state
|
|
105
|
+
* progressed during this session, including transitions such as pending to
|
|
106
|
+
* in_progress, not only completion.
|
|
99
107
|
*
|
|
100
108
|
* When status is `quota` we skip spawning: the orchestrator's handleQuota
|
|
101
109
|
* already scheduled a backoff timer and will call `onQuotaBackoffExpired` once
|
|
102
110
|
* the window closes — that function owns the next spawn in that case.
|
|
103
111
|
*/
|
|
104
|
-
export function onSessionEnded(workspaceId, reason,
|
|
112
|
+
export function onSessionEnded(workspaceId, reason, taskProgressDelta) {
|
|
105
113
|
const row = getRow(workspaceId);
|
|
106
114
|
if (!row)
|
|
107
115
|
return;
|
|
@@ -115,7 +123,7 @@ export function onSessionEnded(workspaceId, reason, tasksDoneDelta) {
|
|
|
115
123
|
// will resume the deferred turn explicitly.
|
|
116
124
|
if (row.status === 'awaiting-user')
|
|
117
125
|
return;
|
|
118
|
-
if (reason === 'error'
|
|
126
|
+
if (reason === 'error') {
|
|
119
127
|
disable(workspaceId, 'error');
|
|
120
128
|
return;
|
|
121
129
|
}
|
|
@@ -126,7 +134,7 @@ export function onSessionEnded(workspaceId, reason, tasksDoneDelta) {
|
|
|
126
134
|
return;
|
|
127
135
|
const db = getDb();
|
|
128
136
|
let streak;
|
|
129
|
-
if (
|
|
137
|
+
if (taskProgressDelta > 0) {
|
|
130
138
|
db.prepare('UPDATE workspaces SET no_progress_streak = 0 WHERE id = ?').run(workspaceId);
|
|
131
139
|
streak = 0;
|
|
132
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|