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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

## v3.0.3 - 2026-08-21

### Fixed

- Removed the multi-kilobyte model-generated title cell from every ordinary turn. ThreadBear now emits the same reviewed policy from its verified binary for a tiny in-app loader, while the mounted Codex app remains the sole title reader and writer.

### Added

- Restored progressive existing-task onboarding after installation: exact historical statuses receive plain icons, conservative first reads receive a quiet `✦`, and unknown tasks stay untouched. Classification is one ephemeral sequential pass, while every title write remains a serial mounted Codex reread/set with exact acknowledgement and no retries or persisted onboarding state.
Expand Down
4 changes: 2 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ After a successful install say:
~/.local/bin/threadbear update --json
```

The managed guidance runs one injection-safe terminal JavaScript cell immediately before an ordinary final response. Replace only the status enum. The cell runs `title --status <complete|next_steps|needs_input|blocked|automation> --json` exactly once; the stateless helper returns the calling task ID and fixed title policy without starting App Server or writing state. The mounted app then reads that exact task, derives one safe desired title, and—only when it differs—calls `tools.codex_app__set_thread_title({title:desired})` once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never retry, start another cell, poll the title, or reconcile.
The managed guidance runs one small terminal JavaScript loader immediately before an ordinary final response. Replace only the status enum. The loader runs `title-script --status <complete|next_steps|needs_input|blocked|automation>` exactly once. That stateless command binds the calling task ID and fixed policy into the reviewed JavaScript program embedded in the verified binary; Codex evaluates it inside the current in-app tool context instead of regenerating the program through the model. The program reads the exact current task through the mounted app and—only when the safe desired title differs—calls the mounted setter once with `threadId` omitted. Exact returned task ID/title is required. If the outer cell yields after 30 seconds, wait only for that same cell; the yield does not cancel a slow native call. Never cache source, retry, start another cell, poll the title, or reconcile.

`update` verifies the official manifest, release URLs, architecture, checksum, embedded version, and candidate self-test before replacement. Network or verification failure leaves the old installation untouched. A later managed-surface write can truthfully leave a rerunnable partial; the binary is written last. Every successful update reports `restart_required`. The daily LaunchAgent runs only this command and never reads tasks or changes titles.

Expand Down Expand Up @@ -317,7 +317,7 @@ Before release, run unit and integration tests, race tests, both Darwin builds,

Release acceptance additionally requires one reviewed candidate live-tested end to end in Codex Desktop:

- the stateless terminal helper works under Codex's default workspace permissions and starts no App Server or title-state write;
- the stateless terminal loader works under Codex's default workspace permissions, remains under 250 source bytes, and starts no App Server or title-state write;
- the mounted app-native reader supplies the exact current title, and the setter receives no explicit current-task ID and returns the exact task ID/title;
- the rendered sidebar shows the expected title before and after a clean restart;
- a full uninstall preview enumerates every unarchived task, confirmed preparation writes no title, and the consented serial app-native pass processes the initiating task last;
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ThreadBear

ThreadBear is a small local title decorator for Codex Desktop. Immediately before each ordinary final response, managed guidance runs one terminal cell: a stateless local helper returns the fixed icon policy, then the mounted Codex app reads and applies the title. ThreadBear changes only the leading status icon.
ThreadBear is a small local title decorator for Codex Desktop. Immediately before each ordinary final response, managed guidance runs one tiny terminal loader: the verified local binary emits its reviewed fixed icon program, Codex evaluates it in the current tool context, and the mounted app reads and applies the title. ThreadBear changes only the leading status icon.

| Mark | Meaning |
| --- | --- |
Expand Down Expand Up @@ -32,7 +32,7 @@ threadbear version

Every command accepts `--json`; the installed binary's `help` output is authoritative.

The terminal `title` command accepts exactly `complete`, `next_steps`, `needs_input`, `blocked`, or `automation`. It returns the calling task ID and fixed icon/safety policy without reading Codex or writing state. In the same cell, the mounted app reads the exact current title, safely derives the subject, and—only when needed—applies one title to the calling task. The exact returned task ID and title must match. A failure stays local and is never retried.
The public `title` command accepts exactly `complete`, `next_steps`, `needs_input`, `blocked`, or `automation` and returns the calling task ID plus fixed icon/safety policy for diagnostics. Ordinary managed guidance uses the internal `title-script` form instead: the verified binary binds that same typed plan into its embedded program, and the 219-byte loader evaluates the complete exit-zero output without caching it. The program reads the exact current title through the mounted app, safely derives the subject, and—only when needed—applies one title to the calling task with `threadId` omitted. The exact returned task ID and title must match. A failure stays local and is never retried.

`uninstall --dry-run --json` asks once for permission to enumerate the complete unarchived App Server catalog and reports which exact ThreadBear prefixes will be removed before managed artifacts. After consent, `uninstall --prepare --noninteractive --confirm --json` takes one fresh complete snapshot. The mounted app rereads each prepared task and makes at most one exact prefix-removal write, with the initiating task last. Any drift or unconfirmed result stops before artifact removal; a fresh rerun makes a new plan. Successful cleanup is followed by `uninstall --commit --noninteractive --confirm --json`, with no final rescan or title call. A bare confirmed uninstall is refused.

Expand Down
75 changes: 5 additions & 70 deletions assets/AGENTS.threadbear.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,14 @@ For every ordinary interactive turn in a main Codex Desktop task:
3. Immediately before the final response, run this one terminal cell. Replace only `STATUS` with the exact enum:

```js
// @exec: {"yield_time_ms": 30000, "max_output_tokens": 1000}
const local = await tools.exec_command({
cmd:"\"$HOME/.local/bin/threadbear\" title --status STATUS --json",
yield_time_ms:30000,
max_output_tokens:1000
const source = await tools.exec_command({
cmd:"\"$HOME/.local/bin/threadbear\" title-script --status STATUS"
});
if (local.exit_code !== 0) { text(local); exit(); }
let plan;
try { plan = JSON.parse(local.output); } catch {
text(JSON.stringify({ready:false, reason:"ThreadBear title helper returned malformed JSON"}));
exit();
}
if (!plan || plan.ready !== true || typeof plan.task_id !== "string" ||
typeof plan.icon !== "string" || !Array.isArray(plan.owned_prefixes) ||
!Array.isArray(plan.blocked_prefixes) || !Array.isArray(plan.internal_markers) ||
!Number.isInteger(plan.max_title_units)) {
text(JSON.stringify({ready:false, reason:"ThreadBear title helper returned an invalid policy"}));
exit();
}
const decodeNative = value => {
if (typeof value !== "string") return value;
try { return JSON.parse(value); } catch { return null; }
};
let current;
try {
current = decodeNative(await tools.codex_app__read_thread({threadId:plan.task_id,
includeOutputs:false,turnLimit:1,maxOutputCharsPerItem:1}));
} catch (error) {
text(JSON.stringify({ready:false, reason:"Codex title read failed", error:String(error)}));
exit();
}
if (!current || current?.thread?.id !== plan.task_id ||
typeof current.thread.title !== "string") {
text(JSON.stringify({ready:false, reason:"Codex title read was not confirmed exactly"}));
exit();
}
const previous = current.thread.title;
if (plan.blocked_prefixes.some(prefix => previous.startsWith(prefix))) {
text(JSON.stringify({ready:false, reason:"The current title has an ambiguous old ThreadBear prefix"}));
exit();
}
let subject = previous;
for (const prefix of plan.owned_prefixes) {
if (subject.startsWith(prefix)) { subject = subject.slice(prefix.length); break; }
}
const lower = subject.toLowerCase();
if (subject.trim() === "" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(subject) ||
plan.internal_markers.some(marker => lower.includes(marker)) ||
(plan.icon + " " + subject).length > plan.max_title_units) {
text(JSON.stringify({ready:false, reason:"The current title is not safe to decorate"}));
exit();
}
const desired = plan.icon + " " + subject;
if (desired === previous) {
text(JSON.stringify({ready:true, task_id:plan.task_id, title:previous, updated:false}));
exit();
}
let renamed;
try {
renamed = decodeNative(await tools.codex_app__set_thread_title({title:desired}));
} catch (error) {
text(JSON.stringify({ready:false, reason:"Codex title write failed", error:String(error)}));
exit();
}
if (!renamed || typeof renamed !== "object" || renamed.threadId !== plan.task_id ||
renamed.title !== desired) {
text(JSON.stringify({ready:false, reason:"Codex title write was not confirmed exactly"}));
exit();
}
text(JSON.stringify({ready:true, task_id:plan.task_id, title:renamed.title, updated:true}));
if (source.exit_code !== 0) { text(source); exit(); }
await (0,eval)(source.output)({tools,text,exit});
Comment thread
openswebot[bot] marked this conversation as resolved.
```

The local command only returns the calling task ID and fixed title policy. The mounted Codex app reads the exact current title and is the sole writer. It receives no explicit task ID when writing, so it can target only the calling task. Make at most one native write attempt. Never run the cell as a progress update. If the outer cell yields, wait only for that same cell; the yield does not cancel a slow native call. Never start another cell, poll the title, retry, or reconcile. A returned failure is local to this turn.
The verified local binary emits its embedded title program with the calling task ID and fixed policy already bound. Codex evaluates that program inside the current in-app tool context; the mounted app reads the exact current title and is the sole writer. The writer receives no explicit task ID, so it can target only the calling task. Make at most one native write attempt. Never run the cell as a progress update. If the outer cell yields, wait only for that same cell; the yield does not cancel a slow native call. Never start another cell, poll the title, retry, or reconcile. A returned failure is local to this turn.

The status controls only the visible icon. ThreadBear emits five exact status prefixes. It also recognizes the five `✦` first-read prefixes and the obsolete neutral bear prefix only so the next ordinary turn can replace them with one exact current status. It preserves every other safe subject and user-authored emoji, and leaves an ambiguous old ThreadBear prefix unchanged rather than guessing. It never puts an owner or action in the title. Use:

Expand Down
3 changes: 3 additions & 0 deletions assets/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ var SkillManagedContent string

//go:embed help.txt
var HelpText string

//go:embed ordinary-title.js
var OrdinaryTitleScript string
4 changes: 2 additions & 2 deletions assets/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Every command accepts --json.
Title:
threadbear title --status <complete|next_steps|needs_input|blocked|automation> --json

The managed guidance runs title exactly once immediately before a final response.
The helper is stateless. The mounted Codex app reads the exact current title and applies at most one icon change. A returned failure stays local and is never retried.
The managed guidance loads one embedded title program immediately before a final response.
The program is stateless. The mounted Codex app reads the exact current title and applies at most one icon change. A returned failure stays local and is never retried.

Install flags:
--dry-run Preview without changing the machine
Expand Down
58 changes: 58 additions & 0 deletions assets/ordinary-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
(plan => async ({tools, text, exit}) => {
const decodeNative = value => {
if (typeof value !== "string") return value;
try { return JSON.parse(value); } catch { return null; }
};
let current;
try {
current = decodeNative(await tools.codex_app__read_thread({
threadId: plan.task_id,
includeOutputs: false,
turnLimit: 1,
maxOutputCharsPerItem: 1,
}));
} catch (error) {
text(JSON.stringify({ready: false, reason: "Codex title read failed", error: String(error)}));
exit();
}
if (!current || current.thread?.id !== plan.task_id || typeof current.thread.title !== "string") {
text(JSON.stringify({ready: false, reason: "Codex title read was not confirmed exactly"}));
exit();
}
const previous = current.thread.title;
if (plan.blocked_prefixes.some(prefix => previous.startsWith(prefix))) {
text(JSON.stringify({ready: false, reason: "The current title has an ambiguous old ThreadBear prefix"}));
exit();
}
let subject = previous;
for (const prefix of plan.owned_prefixes) {
if (subject.startsWith(prefix)) {
subject = subject.slice(prefix.length);
break;
}
}
const lower = subject.toLowerCase();
if (subject.trim() === "" || /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(subject) ||
plan.internal_markers.some(marker => lower.includes(marker)) ||
(plan.icon + " " + subject).length > plan.max_title_units) {
text(JSON.stringify({ready: false, reason: "The current title is not safe to decorate"}));
exit();
}
const desired = plan.icon + " " + subject;
if (desired === previous) {
text(JSON.stringify({ready: true, task_id: plan.task_id, title: previous, updated: false}));
exit();
}
let renamed;
try {
renamed = decodeNative(await tools.codex_app__set_thread_title({title: desired}));
} catch (error) {
text(JSON.stringify({ready: false, reason: "Codex title write failed", error: String(error)}));
exit();
}
if (!renamed || typeof renamed !== "object" || renamed.threadId !== plan.task_id || renamed.title !== desired) {
text(JSON.stringify({ready: false, reason: "Codex title write was not confirmed exactly"}));
exit();
}
text(JSON.stringify({ready: true, task_id: plan.task_id, title: renamed.title, updated: true}));
})
16 changes: 9 additions & 7 deletions cmd/threadbear/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,19 @@ func TestLifecycleNeverTouchesCodexHooks(t *testing.T) {
t.Fatalf("managed AGENTS content = %q", agents)
}
skill, _ := os.ReadFile(p.skill)
for label, text := range map[string]string{"AGENTS": string(agents), "skill": string(skill)} {
if strings.Count(text, "tools.codex_app__set_thread_title") != 1 {
t.Fatalf("%s must contain exactly one mounted app-native setter: %q", label, text)
}
if strings.Count(string(agents), "title-script --status STATUS") != 1 ||
strings.Contains(string(agents), "tools.codex_app__set_thread_title") ||
strings.Count(assets.OrdinaryTitleScript, "tools.codex_app__set_thread_title") != 1 {
t.Fatalf("installed AGENTS/program must define one loader and one mounted setter: AGENTS=%q program=%q", agents, assets.OrdinaryTitleScript)
}
for label, text := range map[string]string{"AGENTS": string(agents), "program": assets.OrdinaryTitleScript, "skill": string(skill)} {
for _, obsolete := range []string{"plan.updated", "plan.unconfirmed", "thread/name/set"} {
if strings.Contains(text, obsolete) {
t.Fatalf("%s contains obsolete detached-writer contract %q", label, obsolete)
}
}
}
if !strings.Contains(string(agents), "plan.owned_prefixes") || !strings.Contains(string(skill), `item.outcome === "prepared"`) {
if !strings.Contains(assets.OrdinaryTitleScript, "plan.owned_prefixes") || !strings.Contains(string(skill), `item.outcome === "prepared"`) {
t.Fatalf("installed guidance lacks planner/prepared contract: AGENTS=%q skill=%q", agents, skill)
}
for _, required := range []string{
Expand All @@ -168,8 +170,8 @@ func TestLifecycleNeverTouchesCodexHooks(t *testing.T) {
"decodeNative(await tools.codex_app__read_thread",
"decodeNative(await tools.codex_app__set_thread_title",
} {
if !strings.Contains(string(agents), required) {
t.Fatalf("installed AGENTS lacks JSON-string native result decoding %q: %q", required, agents)
if !strings.Contains(assets.OrdinaryTitleScript, required) {
t.Fatalf("embedded title program lacks JSON-string native result decoding %q: %q", required, assets.OrdinaryTitleScript)
}
}
for _, required := range []string{
Expand Down
23 changes: 23 additions & 0 deletions cmd/threadbear/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"os"
"strings"

"github.com/ericlitman/threadbear/assets"
)
Expand Down Expand Up @@ -36,6 +37,28 @@ func run(ctx context.Context, args []string, _ io.Reader, stdout, stderr io.Writ
}
return 0
}
if args[0] == "title-script" {
flags := flag.NewFlagSet("title-script", flag.ContinueOnError)
flags.SetOutput(stderr)
selectedStatus := flags.String("status", "", "emit the mounted title program for this status")
if flags.Parse(args[1:]) != nil || flags.NArg() != 0 {
return 2
}
plan, err := runCurrentTitle(ctx, os.Getenv("CODEX_THREAD_ID"), *selectedStatus)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
encoded, err := json.Marshal(plan)
if err != nil {
fmt.Fprintln(stderr, err)
return 1
}
if _, err := fmt.Fprintf(stdout, "(%s)(%s)\n", strings.TrimSpace(assets.OrdinaryTitleScript), encoded); err != nil {
return 1
}
return 0
}
command := args[0]
flags := flag.NewFlagSet(command, flag.ContinueOnError)
flags.SetOutput(stderr)
Expand Down
44 changes: 44 additions & 0 deletions cmd/threadbear/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,50 @@ func TestRunRejectsInvalidTitleStatusBeforeMutation(t *testing.T) {
}
}

func TestRunTitleScriptEmitsOneEmbeddedMountedProgram(t *testing.T) {
t.Setenv("CODEX_THREAD_ID", testTaskID)
var stdout, stderr bytes.Buffer
code := run(t.Context(), []string{"title-script", "--status", "complete"}, strings.NewReader(""), &stdout, &stderr)
if code != 0 || stderr.Len() != 0 {
t.Fatalf("title-script = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String())
}
value := stdout.String()
for _, required := range []string{testTaskID, `"status":"complete"`, `"icon":"✅"`,
"codex_app__read_thread", "codex_app__set_thread_title"} {
if !strings.Contains(value, required) {
t.Fatalf("title-script missing %q: %s", required, value)
}
}
for _, forbidden := range []string{"exec_command", "thread/name/set", "state_N.sqlite"} {
if strings.Contains(value, forbidden) {
t.Fatalf("title-script contains %q: %s", forbidden, value)
}
}
stdout.Reset()
stderr.Reset()
if code = run(t.Context(), []string{"title-script", "--status", "waiting"}, strings.NewReader(""), &stdout, &stderr); code != 1 ||
!strings.Contains(stderr.String(), `unsupported ThreadBear status "waiting"`) || stdout.Len() != 0 {
t.Fatalf("invalid title-script = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String())
}
for name, args := range map[string][]string{
"missing status": {"title-script"},
"extra argument": {"title-script", "--status", "complete", "extra"},
} {
stdout.Reset()
stderr.Reset()
if code = run(t.Context(), args, strings.NewReader(""), &stdout, &stderr); code == 0 || stdout.Len() != 0 {
t.Fatalf("%s = code %d, stdout %q, stderr %q", name, code, stdout.String(), stderr.String())
}
}
t.Setenv("CODEX_THREAD_ID", "")
stdout.Reset()
stderr.Reset()
if code = run(t.Context(), []string{"title-script", "--status", "complete"}, strings.NewReader(""), &stdout, &stderr); code != 1 ||
!strings.Contains(stderr.String(), "CODEX_THREAD_ID is unavailable or invalid") || stdout.Len() != 0 {
t.Fatalf("missing task ID = code %d, stdout %q, stderr %q", code, stdout.String(), stderr.String())
}
}

func TestRunHasNoOnboardCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
code := run(context.Background(), []string{"onboard", "--json"}, strings.NewReader(""), &stdout, &stderr)
Expand Down
Loading
Loading