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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
42 changes: 41 additions & 1 deletion apps/server/src/t3x/autoResume/Reactor.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@ const readModel = (o: {
messages?: Array<{ id: string; role: string }>;
status?: string;
latestTurnId?: string;
/** Explicit latestTurn override; pass null to model an idle thread whose
* projection row has no latest_turn_id (see radroid/t3code#6). */
latestTurn?: { turnId: string; state: string } | null;
}): OrchestrationReadModel =>
({
snapshotSequence: 1,
Expand All@@ -51,7 +54,10 @@ const readModel = (o: {
settledOverride: null,
messages: o.messages ?? [{ id: "u1", role: "user" }],
activities: [],
latestTurn: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
latestTurn:
o.latestTurn !== undefined
? o.latestTurn
: { turnId: o.latestTurnId ?? "turn-1", state: "completed" },
session: { status: o.status ?? "ready", providerName: "claudeAgent" },
},
],
Expand DownExpand Up@@ -201,6 +207,40 @@ describe("AutoResumeReactor (integration)", () => {
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

// Regression for radroid/t3code#6 — the incident shape observed in production:
// the limit lands while the turn is RUNNING (baseline captures its id); by wake
// time the turn has settled and the projection row has no latest_turn_id, so the
// snapshot reports latestTurn: null. That must NOT read as "thread-advanced".
it.effect("resumes when the limited turn has settled away by wake time (latestTurn null)", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(
readModel({ status: "running", latestTurn: { turnId: "turn-1", state: "running" } }),
[rejectedEvent(100)],
);

yield* Effect.gen(function* () {
yield* settle; // detection schedules; baseline.latestTurnId === "turn-1"

// The limited turn settles and the session stops during the wait — the
// projection's latest_turn_id empties out, so the snapshot's latestTurn is null.
yield* Ref.set(modelRef, readModel({ status: "stopped", latestTurn: null }));

yield* advancePastResume;

const commands = yield* Ref.get(dispatched);
const turnStarts = commands.filter((c) => c.type === "thread.turn.start");
assert.strictEqual(turnStarts.length, 1, "the settled thread must resume, not cancel");
const summaries = commands
.filter((c) => c.type === "thread.activity.append")
.map((c) => (c as unknown as { activity: { summary: string } }).activity.summary);
assert.isFalse(
summaries.some((s) => s.includes("thread-advanced")),
"no thread-advanced cancellation may be posted",
);
}).pipe(Effect.provide(AutoResumeReactorLive.pipe(Layer.provideMerge(deps))));
}).pipe(Effect.scoped, Effect.provide(Layer.mergeAll(NodeServices.layer, TestClock.layer()))),
);

it.effect("does NOT resume when the user takes over before the window reopens", () =>
Effect.gen(function* () {
const { dispatched, modelRef, deps } = yield* harness(readModel({}), [rejectedEvent(100)]);
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/t3x/autoResume/guards.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,25 @@ describe("cancelReason", () => {
expect(cancelReason(thread, baseline())).toBe("thread-advanced");
});

// Regression for radroid/t3code#6: the projection populates latest_turn_id only while
// a turn is active, so a limit captured mid-turn (baseline has the running turn's id)
// always sees latestTurn: null once that turn settles. Null is "no active turn", not
// advancement — cancelling here killed every real-world resume.
it("does NOT treat a settled-away turn (latestTurn null at fire) as advancement", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
status: "stopped",
});
expect(thread.latestTurn).toBeNull();
expect(cancelReason(thread, baseline())).toBeNull();
});

it("still detects advancement when a turn exists but the baseline had none", () => {
const noTurnBaseline = captureBaseline(makeThread({ messages: [{ id: "u1", role: "user" }] }));
const thread = makeThread({ messages: [{ id: "u1", role: "user" }], latestTurnId: "turn-9" });
expect(cancelReason(thread, noTurnBaseline)).toBe("thread-advanced");
});

it("blocks when awaiting input", () => {
const thread = makeThread({
messages: [{ id: "u1", role: "user" }],
Expand Down
13 changes: 12 additions & 1 deletion apps/server/src/t3x/autoResume/guards.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -130,6 +130,17 @@ export function cancelReason(
if (threadIsProgressing(thread)) return "progressing";
if (hasOpenBlockingRequest(thread.activities)) return "awaiting-input";
if (newestUserMessageId(thread) !== baseline.newestUserMessageId) return "user-took-over";
if ((thread.latestTurn?.turnId ?? null) !== baseline.latestTurnId) return "thread-advanced";
// Advancement needs POSITIVE evidence: a different, non-null turn id. The snapshot's
// `latestTurn` is joined on `projection_threads.latest_turn_id`, which is populated
// only while a turn is active — so a usage limit that lands mid-turn captures the
// running turn's id in the baseline, and by fire time (turn settled, session idle)
// the snapshot reports `latestTurn: null`. That null means "no active turn", not
// "the thread moved on"; treating it as advancement cancelled every real resume
// (radroid/t3code#6). A genuine user takeover is caught above via the newest user
// message; active work is caught by `progressing`.
const currentTurnId = thread.latestTurn?.turnId ?? null;
if (currentTurnId !== null && currentTurnId !== baseline.latestTurnId) {
return "thread-advanced";
}
return null;
}