open-research-protocol 0.4.29 → 0.4.31
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 +26 -0
- package/README.md +5 -4
- package/cli/orp.py +26 -21
- package/docs/RESEARCH_COUNCIL.md +5 -5
- package/package.json +1 -1
- package/packages/orp-workspace-launcher/src/codex.js +35 -3
- package/packages/orp-workspace-launcher/src/tabs.js +31 -15
- package/packages/orp-workspace-launcher/test/codex.test.js +32 -0
- package/packages/orp-workspace-launcher/test/commands.test.js +3 -3
- package/packages/orp-workspace-launcher/test/tabs.test.js +144 -7
- package/cli/__pycache__/orp.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-pilot.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-agent-replication.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-benchmark.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-canonical-continuation.cpython-311.pyc +0 -0
- package/scripts/__pycache__/orp-kernel-continuation-pilot.cpython-311.pyc +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ There was no prior in-repo changelog file, so the first formal entry starts
|
|
|
6
6
|
with the currently shipped `v0.4.4` release and summarizes the full release
|
|
7
7
|
delta reflected in this repo.
|
|
8
8
|
|
|
9
|
+
## v0.4.31 - 2026-04-25
|
|
10
|
+
|
|
11
|
+
This release refreshes ORP's OpenAI-backed research lanes and tightens
|
|
12
|
+
workspace tab recency ranking for grouped project sessions.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Updated built-in OpenAI research profiles to use `gpt-5.5` for high-reasoning,
|
|
17
|
+
web synthesis, and pro-research-style lanes, with Responses API `web_search`
|
|
18
|
+
and `xhigh` reasoning on deep research passes.
|
|
19
|
+
- Workspace tab reports now rank grouped Codex project tabs by the freshest
|
|
20
|
+
tracked session update time while keeping same-project sessions together.
|
|
21
|
+
|
|
22
|
+
## v0.4.30 - 2026-04-25
|
|
23
|
+
|
|
24
|
+
This release tightens ORP-managed Codex session tracking so short-lived
|
|
25
|
+
`codex exec` runs, including Clawdad summary and planning work, do not replace
|
|
26
|
+
the saved interactive workspace thread.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `orp codex status` and `orp codex reconcile` now ignore local Codex exec
|
|
31
|
+
sessions by default, alongside the existing delegated/subagent filtering.
|
|
32
|
+
- Added `--include-exec` to the ORP/Codex status and reconcile flows for
|
|
33
|
+
explicit diagnostics when exec sessions need to be inspected.
|
|
34
|
+
|
|
9
35
|
## v0.4.29 - 2026-04-25
|
|
10
36
|
|
|
11
37
|
This release adds ORP-managed Codex session tracking so starting a Codex thread
|
package/README.md
CHANGED
|
@@ -473,10 +473,11 @@ orp schedule add codex --name morning-summary --prompt "Summarize this repo" --j
|
|
|
473
473
|
aligned with Codex sessions. `status` compares the current repo against the
|
|
474
474
|
latest local Codex session metadata, `reconcile` refreshes stale saved sessions,
|
|
475
475
|
and bare `orp codex` launches Codex from the repo root while watching for the new
|
|
476
|
-
session id.
|
|
477
|
-
artifact-output repos should be left untracked when a separate lab repo is
|
|
478
|
-
source of truth. Use
|
|
479
|
-
|
|
476
|
+
session id. Codex exec and delegated/subagent sessions are ignored by default,
|
|
477
|
+
and artifact-output repos should be left untracked when a separate lab repo is
|
|
478
|
+
the source of truth. Use `--include-exec` only when diagnosing short-lived exec
|
|
479
|
+
sessions, and use `--` before Codex args that conflict with ORP wrapper options.
|
|
480
|
+
The manual fallback from inside Codex is still:
|
|
480
481
|
|
|
481
482
|
```bash
|
|
482
483
|
orp workspace add-tab main --here --current-codex
|
package/cli/orp.py
CHANGED
|
@@ -141,6 +141,8 @@ FRONTIER_TERMINAL_STATUSES = {"complete", "completed", "done", "skipped", "termi
|
|
|
141
141
|
YOUTUBE_SOURCE_SCHEMA_VERSION = "1.0.0"
|
|
142
142
|
EXCHANGE_REPORT_SCHEMA_VERSION = "1.0.0"
|
|
143
143
|
RESEARCH_RUN_SCHEMA_VERSION = "1.0.0"
|
|
144
|
+
OPENAI_RESEARCH_MODEL = "gpt-5.5"
|
|
145
|
+
OPENAI_DEEP_RESEARCH_MODEL = OPENAI_RESEARCH_MODEL
|
|
144
146
|
SECRET_SPEND_POLICY_SCHEMA_VERSION = "1.0.0"
|
|
145
147
|
RESEARCH_SPEND_LEDGER_SCHEMA_VERSION = "1.0.0"
|
|
146
148
|
PROJECT_CONTEXT_SCHEMA_VERSION = "1.0.0"
|
|
@@ -10661,23 +10663,23 @@ def _project_research_trigger_policy() -> dict[str, Any]:
|
|
|
10661
10663
|
"moment_id": "thinking_reasoning_high",
|
|
10662
10664
|
"calls_api": True,
|
|
10663
10665
|
"lane": "openai_reasoning_high",
|
|
10664
|
-
"model":
|
|
10666
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
10665
10667
|
"when": "Use when the directory has a decision gate, route choice, proof strategy, architecture tradeoff, or ambiguous next action.",
|
|
10666
10668
|
},
|
|
10667
10669
|
{
|
|
10668
10670
|
"moment_id": "web_synthesis",
|
|
10669
10671
|
"calls_api": True,
|
|
10670
10672
|
"lane": "openai_web_synthesis",
|
|
10671
|
-
"model":
|
|
10673
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
10672
10674
|
"when": "Use when the answer depends on current public facts, external docs, papers, project status, or citations.",
|
|
10673
10675
|
},
|
|
10674
10676
|
{
|
|
10675
10677
|
"moment_id": "pro_deep_research",
|
|
10676
10678
|
"calls_api": True,
|
|
10677
10679
|
"lane": "openai_deep_research",
|
|
10678
|
-
"model":
|
|
10680
|
+
"model": OPENAI_DEEP_RESEARCH_MODEL,
|
|
10679
10681
|
"when": "Use only after reasoning/web lanes expose a research-heavy gap, disagreement, source-quality issue, or literature-scale synthesis need.",
|
|
10680
|
-
"capability_note": "
|
|
10682
|
+
"capability_note": "Runs GPT-5.5 with background mode, web search, and xhigh reasoning for a pro-research-style pass.",
|
|
10681
10683
|
},
|
|
10682
10684
|
],
|
|
10683
10685
|
"skip_research_when": [
|
|
@@ -17656,7 +17658,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17656
17658
|
"calls_api": True,
|
|
17657
17659
|
"secret_alias": "openai-primary",
|
|
17658
17660
|
"env_var": "OPENAI_API_KEY",
|
|
17659
|
-
"description": "Call
|
|
17661
|
+
"description": f"Call {OPENAI_RESEARCH_MODEL} with high reasoning to critique and compress the opening research.",
|
|
17660
17662
|
},
|
|
17661
17663
|
{
|
|
17662
17664
|
"moment_id": "think_web_crosscheck",
|
|
@@ -17664,7 +17666,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17664
17666
|
"calls_api": True,
|
|
17665
17667
|
"secret_alias": "openai-primary",
|
|
17666
17668
|
"env_var": "OPENAI_API_KEY",
|
|
17667
|
-
"description": "Call
|
|
17669
|
+
"description": f"Call {OPENAI_RESEARCH_MODEL} with high reasoning and web search to verify recency-sensitive claims.",
|
|
17668
17670
|
},
|
|
17669
17671
|
{
|
|
17670
17672
|
"moment_id": "think_synthesis",
|
|
@@ -17672,7 +17674,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17672
17674
|
"calls_api": True,
|
|
17673
17675
|
"secret_alias": "openai-primary",
|
|
17674
17676
|
"env_var": "OPENAI_API_KEY",
|
|
17675
|
-
"description": "Call
|
|
17677
|
+
"description": f"Call {OPENAI_RESEARCH_MODEL} with high reasoning to resolve disagreements before final research.",
|
|
17676
17678
|
},
|
|
17677
17679
|
{
|
|
17678
17680
|
"moment_id": "final_deep_research",
|
|
@@ -17691,7 +17693,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17691
17693
|
"call_moment": "opening_deep_research",
|
|
17692
17694
|
"label": "Opening Deep Research",
|
|
17693
17695
|
"provider": "openai",
|
|
17694
|
-
"model":
|
|
17696
|
+
"model": OPENAI_DEEP_RESEARCH_MODEL,
|
|
17695
17697
|
"adapter": "openai_responses",
|
|
17696
17698
|
"role": (
|
|
17697
17699
|
"Initial Deep Research scan. Map the landscape, source families, hard unknowns, "
|
|
@@ -17712,9 +17714,10 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17712
17714
|
],
|
|
17713
17715
|
"env_var": "OPENAI_API_KEY",
|
|
17714
17716
|
"secret_alias": "openai-primary",
|
|
17717
|
+
"reasoning_effort": "xhigh",
|
|
17715
17718
|
"reasoning_summary": "auto",
|
|
17716
17719
|
"web_search": True,
|
|
17717
|
-
"web_search_tool": "
|
|
17720
|
+
"web_search_tool": "web_search",
|
|
17718
17721
|
"background": False,
|
|
17719
17722
|
"spend_reserve_usd": 1.5,
|
|
17720
17723
|
"max_tool_calls": 40,
|
|
@@ -17728,7 +17731,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17728
17731
|
"call_moment": "think_after_deep",
|
|
17729
17732
|
"label": "Think after Deep Research",
|
|
17730
17733
|
"provider": "openai",
|
|
17731
|
-
"model":
|
|
17734
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
17732
17735
|
"adapter": "openai_responses",
|
|
17733
17736
|
"role": (
|
|
17734
17737
|
"High-reasoning critique of the opening Deep Research output. Compress it into a sharper "
|
|
@@ -17761,7 +17764,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17761
17764
|
"call_moment": "think_web_crosscheck",
|
|
17762
17765
|
"label": "Think with web cross-check",
|
|
17763
17766
|
"provider": "openai",
|
|
17764
|
-
"model":
|
|
17767
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
17765
17768
|
"adapter": "openai_responses",
|
|
17766
17769
|
"role": (
|
|
17767
17770
|
"High-reasoning web-search pass. Verify current facts, citations, public claims, "
|
|
@@ -17799,7 +17802,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17799
17802
|
"call_moment": "think_synthesis",
|
|
17800
17803
|
"label": "Synthesis thinking pass",
|
|
17801
17804
|
"provider": "openai",
|
|
17802
|
-
"model":
|
|
17805
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
17803
17806
|
"adapter": "openai_responses",
|
|
17804
17807
|
"role": (
|
|
17805
17808
|
"High-reasoning synthesis pass. Reconcile the deep-research map, critique, and web cross-check "
|
|
@@ -17831,7 +17834,7 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17831
17834
|
"call_moment": "final_deep_research",
|
|
17832
17835
|
"label": "Final Deep Research",
|
|
17833
17836
|
"provider": "openai",
|
|
17834
|
-
"model":
|
|
17837
|
+
"model": OPENAI_DEEP_RESEARCH_MODEL,
|
|
17835
17838
|
"adapter": "openai_responses",
|
|
17836
17839
|
"role": (
|
|
17837
17840
|
"Final Deep Research pass. Use all prior lane outputs to produce the decisive, source-grounded "
|
|
@@ -17851,9 +17854,10 @@ def _research_staged_deep_think_profile(profile_id: str = "deep-think-web-think-
|
|
|
17851
17854
|
],
|
|
17852
17855
|
"env_var": "OPENAI_API_KEY",
|
|
17853
17856
|
"secret_alias": "openai-primary",
|
|
17857
|
+
"reasoning_effort": "xhigh",
|
|
17854
17858
|
"reasoning_summary": "auto",
|
|
17855
17859
|
"web_search": True,
|
|
17856
|
-
"web_search_tool": "
|
|
17860
|
+
"web_search_tool": "web_search",
|
|
17857
17861
|
"background": False,
|
|
17858
17862
|
"spend_reserve_usd": 1.5,
|
|
17859
17863
|
"max_tool_calls": 40,
|
|
@@ -17901,7 +17905,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17901
17905
|
"calls_api": True,
|
|
17902
17906
|
"secret_alias": "openai-primary",
|
|
17903
17907
|
"env_var": "OPENAI_API_KEY",
|
|
17904
|
-
"description": "Call
|
|
17908
|
+
"description": f"Call {OPENAI_RESEARCH_MODEL} with high reasoning for the deliberate thinking pass.",
|
|
17905
17909
|
},
|
|
17906
17910
|
{
|
|
17907
17911
|
"moment_id": "web_synthesis",
|
|
@@ -17909,7 +17913,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17909
17913
|
"calls_api": True,
|
|
17910
17914
|
"secret_alias": "openai-primary",
|
|
17911
17915
|
"env_var": "OPENAI_API_KEY",
|
|
17912
|
-
"description": "Call
|
|
17916
|
+
"description": f"Call {OPENAI_RESEARCH_MODEL} with web search for current public evidence and citations.",
|
|
17913
17917
|
},
|
|
17914
17918
|
{
|
|
17915
17919
|
"moment_id": "pro_deep_research",
|
|
@@ -17926,7 +17930,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17926
17930
|
"call_moment": "thinking_reasoning_high",
|
|
17927
17931
|
"label": "OpenAI reasoning high",
|
|
17928
17932
|
"provider": "openai",
|
|
17929
|
-
"model":
|
|
17933
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
17930
17934
|
"adapter": "openai_responses",
|
|
17931
17935
|
"role": "Deliberate high-reasoning pass from the provided context. Think hard, critique assumptions, and produce a decision-oriented answer.",
|
|
17932
17936
|
"env_var": "OPENAI_API_KEY",
|
|
@@ -17941,7 +17945,7 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17941
17945
|
"call_moment": "web_synthesis",
|
|
17942
17946
|
"label": "OpenAI web synthesis",
|
|
17943
17947
|
"provider": "openai",
|
|
17944
|
-
"model":
|
|
17948
|
+
"model": OPENAI_RESEARCH_MODEL,
|
|
17945
17949
|
"adapter": "openai_responses",
|
|
17946
17950
|
"role": "Recency-aware synthesis using OpenAI Responses web search with citations.",
|
|
17947
17951
|
"env_var": "OPENAI_API_KEY",
|
|
@@ -17961,14 +17965,15 @@ def _research_default_profile(profile_id: str = "openai-council") -> dict[str, A
|
|
|
17961
17965
|
"call_moment": "pro_deep_research",
|
|
17962
17966
|
"label": "OpenAI Pro / Deep Research",
|
|
17963
17967
|
"provider": "openai",
|
|
17964
|
-
"model":
|
|
17968
|
+
"model": OPENAI_DEEP_RESEARCH_MODEL,
|
|
17965
17969
|
"adapter": "openai_responses",
|
|
17966
17970
|
"role": "Pro Research style long-form investigation. Produce a structured, citation-rich report grounded in public sources.",
|
|
17967
17971
|
"env_var": "OPENAI_API_KEY",
|
|
17968
17972
|
"secret_alias": "openai-primary",
|
|
17973
|
+
"reasoning_effort": "xhigh",
|
|
17969
17974
|
"reasoning_summary": "auto",
|
|
17970
17975
|
"web_search": True,
|
|
17971
|
-
"web_search_tool": "
|
|
17976
|
+
"web_search_tool": "web_search",
|
|
17972
17977
|
"background": True,
|
|
17973
17978
|
"spend_reserve_usd": 3.5,
|
|
17974
17979
|
"max_tool_calls": 40,
|
|
@@ -18914,7 +18919,7 @@ def _research_run_openai_lane(
|
|
|
18914
18919
|
}
|
|
18915
18920
|
|
|
18916
18921
|
body: dict[str, Any] = {
|
|
18917
|
-
"model": str(lane.get("model",
|
|
18922
|
+
"model": str(lane.get("model", OPENAI_RESEARCH_MODEL)).strip() or OPENAI_RESEARCH_MODEL,
|
|
18918
18923
|
"input": prompt,
|
|
18919
18924
|
"background": bool(lane.get("background", False)),
|
|
18920
18925
|
}
|
package/docs/RESEARCH_COUNCIL.md
CHANGED
|
@@ -22,11 +22,11 @@ orp research ask "Where should this system live?" --execute --json
|
|
|
22
22
|
|
|
23
23
|
The built-in `openai-council` profile defines three OpenAI API lanes:
|
|
24
24
|
|
|
25
|
-
- `openai_reasoning_high`: `gpt-5.
|
|
26
|
-
- `openai_web_synthesis`: `gpt-5.
|
|
27
|
-
- `openai_deep_research`: `
|
|
25
|
+
- `openai_reasoning_high`: `gpt-5.5` with `reasoning.effort=high` for the deliberate thinking pass.
|
|
26
|
+
- `openai_web_synthesis`: `gpt-5.5` with high reasoning plus Responses API web search for current public evidence and citations.
|
|
27
|
+
- `openai_deep_research`: `gpt-5.5` with `reasoning.effort=xhigh`, background execution, and Responses API web search for Pro/Deep Research style investigation.
|
|
28
28
|
|
|
29
|
-
This follows OpenAI's current model guidance: `gpt-5.
|
|
29
|
+
This follows OpenAI's current model guidance: `gpt-5.5` works best through the Responses API for reasoning and tool workflows; web search is enabled through the Responses API `tools` array when current information is needed; and deeper research-style work should use higher reasoning effort plus background mode.
|
|
30
30
|
|
|
31
31
|
## Staged Deep Research Template
|
|
32
32
|
|
|
@@ -119,7 +119,7 @@ Fixtures are useful when an OpenAI run happened outside ORP, when you are compar
|
|
|
119
119
|
|
|
120
120
|
ORP uses the Responses API for these lanes. Useful knobs in profile JSON:
|
|
121
121
|
|
|
122
|
-
- `model`: for example `gpt-5.
|
|
122
|
+
- `model`: for example `gpt-5.5`.
|
|
123
123
|
- `call_moment`: the named research-loop moment when this lane may resolve a key.
|
|
124
124
|
- `reasoning_effort`: `none`, `low`, `medium`, `high`, or `xhigh` for supported models.
|
|
125
125
|
- `reasoning_summary`: `auto` or `detailed` for Deep Research reasoning summaries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-research-protocol",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
4
4
|
"description": "ORP CLI (Open Research Protocol): workspace ledgers, secrets, scheduling, governed execution, and agent-friendly research workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fractal Research Group <cody@frg.earth>",
|
|
@@ -77,6 +77,22 @@ export function isDelegatedCodexSession(session) {
|
|
|
77
77
|
return sourceText.includes("subagent") || sourceText.includes("delegate");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export function isCodexExecSession(session) {
|
|
81
|
+
const originator = normalizeOptionalString(session?.originator)?.toLowerCase();
|
|
82
|
+
if (originator === "codex_exec" || originator === "codex-exec") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const source = session?.source;
|
|
86
|
+
if (typeof source === "string") {
|
|
87
|
+
return source.trim().toLowerCase() === "exec";
|
|
88
|
+
}
|
|
89
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const sourceKind = normalizeOptionalString(source.kind ?? source.type ?? source.source)?.toLowerCase();
|
|
93
|
+
return sourceKind === "exec";
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
export function parseCodexSessionMetaLine(line, filePath, stat = {}) {
|
|
81
97
|
let row;
|
|
82
98
|
try {
|
|
@@ -97,6 +113,12 @@ export function parseCodexSessionMetaLine(line, filePath, stat = {}) {
|
|
|
97
113
|
|
|
98
114
|
const timestamp = normalizeOptionalString(payload.timestamp) || normalizeOptionalString(row.timestamp);
|
|
99
115
|
const timestampMs = timestamp ? Date.parse(timestamp) : 0;
|
|
116
|
+
const source =
|
|
117
|
+
typeof payload.source === "string"
|
|
118
|
+
? normalizeOptionalString(payload.source)
|
|
119
|
+
: payload.source && typeof payload.source === "object" && !Array.isArray(payload.source)
|
|
120
|
+
? payload.source
|
|
121
|
+
: null;
|
|
100
122
|
return {
|
|
101
123
|
sessionId,
|
|
102
124
|
cwd: path.resolve(cwd),
|
|
@@ -106,7 +128,7 @@ export function parseCodexSessionMetaLine(line, filePath, stat = {}) {
|
|
|
106
128
|
filePath,
|
|
107
129
|
originator: normalizeOptionalString(payload.originator),
|
|
108
130
|
cliVersion: normalizeOptionalString(payload.cli_version ?? payload.cliVersion),
|
|
109
|
-
source
|
|
131
|
+
source,
|
|
110
132
|
};
|
|
111
133
|
}
|
|
112
134
|
|
|
@@ -171,6 +193,7 @@ export async function scanCodexSessions(options = {}) {
|
|
|
171
193
|
const sessionsDir = path.join(codexHome, "sessions");
|
|
172
194
|
const sinceMs = typeof options.sinceMs === "number" ? options.sinceMs : 0;
|
|
173
195
|
const includeDelegated = Boolean(options.includeDelegated || options.includeSubagents);
|
|
196
|
+
const includeExec = Boolean(options.includeExec || options.includeCodexExec);
|
|
174
197
|
const files = await walkSessionFiles(sessionsDir, options);
|
|
175
198
|
const sessions = [];
|
|
176
199
|
|
|
@@ -192,6 +215,9 @@ export async function scanCodexSessions(options = {}) {
|
|
|
192
215
|
if (!session) {
|
|
193
216
|
continue;
|
|
194
217
|
}
|
|
218
|
+
if (!includeExec && isCodexExecSession(session)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
195
221
|
if (!includeDelegated && isDelegatedCodexSession(session)) {
|
|
196
222
|
continue;
|
|
197
223
|
}
|
|
@@ -287,6 +313,7 @@ function sessionSummary(session) {
|
|
|
287
313
|
updatedAt: session.updatedMs ? new Date(session.updatedMs).toISOString() : null,
|
|
288
314
|
originator: session.originator || null,
|
|
289
315
|
cliVersion: session.cliVersion || null,
|
|
316
|
+
source: session.source || null,
|
|
290
317
|
filePath: session.filePath || null,
|
|
291
318
|
};
|
|
292
319
|
}
|
|
@@ -563,6 +590,10 @@ function parseCommonOptions(argv = [], defaults = {}, parseOptions = {}) {
|
|
|
563
590
|
options.includeDelegated = true;
|
|
564
591
|
continue;
|
|
565
592
|
}
|
|
593
|
+
if (arg === "--include-exec") {
|
|
594
|
+
options.includeExec = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
566
597
|
if (arg === "--include-artifacts") {
|
|
567
598
|
options.includeArtifactRepos = true;
|
|
568
599
|
continue;
|
|
@@ -658,6 +689,7 @@ Commands:
|
|
|
658
689
|
start Launch Codex in the repo root and save the new session when metadata appears
|
|
659
690
|
|
|
660
691
|
Notes:
|
|
692
|
+
- Codex exec sessions are ignored by default.
|
|
661
693
|
- Delegated/subagent sessions are ignored by default.
|
|
662
694
|
- Broad roots and artifact-output repos are refused unless explicitly overridden.
|
|
663
695
|
- Use -- before Codex args that conflict with ORP wrapper options.
|
|
@@ -669,7 +701,7 @@ function printStatusHelp() {
|
|
|
669
701
|
console.log(`ORP Codex status
|
|
670
702
|
|
|
671
703
|
Usage:
|
|
672
|
-
orp codex status [--workspace main] [--path <repo-or-subdir>] [--codex-home <path>] [--json]
|
|
704
|
+
orp codex status [--workspace main] [--path <repo-or-subdir>] [--codex-home <path>] [--include-exec] [--json]
|
|
673
705
|
`);
|
|
674
706
|
}
|
|
675
707
|
|
|
@@ -677,7 +709,7 @@ function printReconcileHelp() {
|
|
|
677
709
|
console.log(`ORP Codex reconcile
|
|
678
710
|
|
|
679
711
|
Usage:
|
|
680
|
-
orp codex reconcile [--workspace main] [--dry-run] [--add-missing] [--since-days <n>] [--json]
|
|
712
|
+
orp codex reconcile [--workspace main] [--dry-run] [--add-missing] [--since-days <n>] [--include-exec] [--json]
|
|
681
713
|
`);
|
|
682
714
|
}
|
|
683
715
|
|
|
@@ -122,9 +122,9 @@ function buildCodexActivityIndex(tabs = [], options = {}) {
|
|
|
122
122
|
return activityBySessionId;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
125
|
+
function buildRankedTabs(tabs = [], options = {}) {
|
|
126
126
|
const activityBySessionId = buildCodexActivityIndex(tabs, options);
|
|
127
|
-
|
|
127
|
+
return tabs.map((tab, originalIndex) => {
|
|
128
128
|
const sessionActivity =
|
|
129
129
|
tab.resumeTool === "codex" && tab.sessionId ? activityBySessionId.get(String(tab.sessionId).toLowerCase()) : null;
|
|
130
130
|
return {
|
|
@@ -133,24 +133,40 @@ function orderTabsByRecentActivity(tabs = [], options = {}) {
|
|
|
133
133
|
activityMs: sessionActivity?.mtimeMs || 0,
|
|
134
134
|
};
|
|
135
135
|
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function orderTabsByRecentActivity(tabs = [], options = {}) {
|
|
139
|
+
const rankedTabs = buildRankedTabs(tabs, options);
|
|
140
|
+
const projects = new Map();
|
|
136
141
|
|
|
137
|
-
const projectActivity = new Map();
|
|
138
142
|
for (const ranked of rankedTabs) {
|
|
139
|
-
const
|
|
140
|
-
|
|
143
|
+
const projectPath = ranked.tab.path;
|
|
144
|
+
if (!projects.has(projectPath)) {
|
|
145
|
+
projects.set(projectPath, {
|
|
146
|
+
projectPath,
|
|
147
|
+
firstIndex: ranked.originalIndex,
|
|
148
|
+
activityMs: ranked.activityMs,
|
|
149
|
+
tabs: [],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const project = projects.get(projectPath);
|
|
154
|
+
project.firstIndex = Math.min(project.firstIndex, ranked.originalIndex);
|
|
155
|
+
project.activityMs = Math.max(project.activityMs, ranked.activityMs);
|
|
156
|
+
project.tabs.push(ranked);
|
|
141
157
|
}
|
|
142
158
|
|
|
143
|
-
return
|
|
144
|
-
.sort(
|
|
145
|
-
|
|
146
|
-
const rightProjectActivity = projectActivity.get(right.tab.path) || 0;
|
|
147
|
-
return (
|
|
148
|
-
rightProjectActivity - leftProjectActivity ||
|
|
159
|
+
return [...projects.values()]
|
|
160
|
+
.sort(
|
|
161
|
+
(left, right) =>
|
|
149
162
|
right.activityMs - left.activityMs ||
|
|
150
|
-
left.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
left.firstIndex - right.firstIndex,
|
|
164
|
+
)
|
|
165
|
+
.flatMap((project) =>
|
|
166
|
+
project.tabs
|
|
167
|
+
.sort((left, right) => right.activityMs - left.activityMs || left.originalIndex - right.originalIndex)
|
|
168
|
+
.map((ranked) => ranked.tab),
|
|
169
|
+
);
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
export function parseWorkspaceTabsArgs(argv = []) {
|
|
@@ -129,6 +129,38 @@ test("scanCodexSessions ignores delegated sessions by default", async () => {
|
|
|
129
129
|
);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("scanCodexSessions ignores exec sessions by default", async () => {
|
|
133
|
+
const tempDir = await makeTempDir();
|
|
134
|
+
const codexHome = path.join(tempDir, "codex-home");
|
|
135
|
+
const repoRoot = path.join(tempDir, "repo");
|
|
136
|
+
await writeSession(
|
|
137
|
+
codexHome,
|
|
138
|
+
"019dc2cb-d435-7072-bbfd-4ae4280474d1",
|
|
139
|
+
repoRoot,
|
|
140
|
+
"2026-04-25T12:00:00Z",
|
|
141
|
+
);
|
|
142
|
+
await writeSession(
|
|
143
|
+
codexHome,
|
|
144
|
+
"019dc2cb-d435-7072-bbfd-4ae4280474d2",
|
|
145
|
+
repoRoot,
|
|
146
|
+
"2026-04-25T12:01:00Z",
|
|
147
|
+
{ originator: "codex_exec", source: "exec" },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const defaultSessions = await scanCodexSessions({ codexHome, sinceMs: 0 });
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
defaultSessions.map((session) => session.sessionId),
|
|
153
|
+
["019dc2cb-d435-7072-bbfd-4ae4280474d1"],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const withExecSessions = await scanCodexSessions({ codexHome, sinceMs: 0, includeExec: true });
|
|
157
|
+
assert.deepEqual(
|
|
158
|
+
withExecSessions.map((session) => session.sessionId),
|
|
159
|
+
["019dc2cb-d435-7072-bbfd-4ae4280474d2", "019dc2cb-d435-7072-bbfd-4ae4280474d1"],
|
|
160
|
+
);
|
|
161
|
+
assert.equal(withExecSessions[0].source, "exec");
|
|
162
|
+
});
|
|
163
|
+
|
|
132
164
|
test("scanCodexSessions finds session metadata near the start of a rollout file", async () => {
|
|
133
165
|
const tempDir = await makeTempDir();
|
|
134
166
|
const codexHome = path.join(tempDir, "codex-home");
|
|
@@ -74,12 +74,12 @@ test("buildWorkspaceCommandsReport exposes direct restart commands and exact sav
|
|
|
74
74
|
assert.equal(report.commandCount, 3);
|
|
75
75
|
assert.equal(report.tabs[0]?.resumeCommand, "codex resume abc-123");
|
|
76
76
|
assert.equal(report.tabs[0]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration' && codex resume abc-123");
|
|
77
|
-
assert.equal(report.tabs[1]?.
|
|
77
|
+
assert.equal(report.tabs[1]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration'");
|
|
78
|
+
assert.equal(report.tabs[2]?.resumeCommand, "claude resume claude-456");
|
|
78
79
|
assert.equal(
|
|
79
|
-
report.tabs[
|
|
80
|
+
report.tabs[2]?.restartCommand,
|
|
80
81
|
"cd '/Volumes/Code_2TB/code/anthropic-lab' && claude resume claude-456",
|
|
81
82
|
);
|
|
82
|
-
assert.equal(report.tabs[2]?.restartCommand, "cd '/Volumes/Code_2TB/code/collaboration'");
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test("runWorkspaceCommands prints JSON with copyable commands", async () => {
|
|
@@ -111,16 +111,16 @@ test("buildWorkspaceTabsReport keeps duplicate titles unique and exposes generic
|
|
|
111
111
|
"cd '/Volumes/Code_2TB/code/collaboration' && codex resume abc-123",
|
|
112
112
|
);
|
|
113
113
|
assert.equal(report.tabs[0]?.codexSessionId, "abc-123");
|
|
114
|
-
assert.equal(report.tabs[1]?.title, "
|
|
115
|
-
assert.equal(report.tabs[1]?.
|
|
116
|
-
assert.equal(report.tabs[
|
|
114
|
+
assert.equal(report.tabs[1]?.title, "collaboration (2)");
|
|
115
|
+
assert.equal(report.tabs[1]?.codexSessionId, null);
|
|
116
|
+
assert.equal(report.tabs[2]?.title, "anthropic-lab");
|
|
117
|
+
assert.equal(report.tabs[2]?.resumeCommand, "claude resume claude-456");
|
|
118
|
+
assert.equal(report.tabs[2]?.remoteBranch, "main");
|
|
117
119
|
assert.equal(
|
|
118
|
-
report.tabs[
|
|
120
|
+
report.tabs[2]?.restartCommand,
|
|
119
121
|
"cd '/Volumes/Code_2TB/code/anthropic-lab' && claude resume claude-456",
|
|
120
122
|
);
|
|
121
|
-
assert.equal(report.tabs[
|
|
122
|
-
assert.equal(report.tabs[2]?.title, "collaboration (2)");
|
|
123
|
-
assert.equal(report.tabs[2]?.codexSessionId, null);
|
|
123
|
+
assert.equal(report.tabs[2]?.claudeSessionId, "claude-456");
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
test("buildWorkspaceTabsReport ranks Codex tabs by recent local session activity", async () => {
|
|
@@ -183,6 +183,143 @@ test("buildWorkspaceTabsReport ranks Codex tabs by recent local session activity
|
|
|
183
183
|
assert.equal(report.projects[2]?.path, "/Volumes/Code_2TB/code/no-session-project");
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
test("buildWorkspaceTabsReport ranks tracked Codex sessions by update time, not rollout creation time", async () => {
|
|
187
|
+
const tempDir = await makeTempDir();
|
|
188
|
+
const codexHome = path.join(tempDir, "codex-home");
|
|
189
|
+
const sessionsDir = path.join(codexHome, "sessions", "2026", "04", "15");
|
|
190
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
191
|
+
|
|
192
|
+
const oldRolloutUpdatedSessionId = "019d0000-0000-7000-8000-000000000021";
|
|
193
|
+
const newRolloutStaleSessionId = "019d0000-0000-7000-8000-000000000022";
|
|
194
|
+
const untrackedFreshSessionId = "019d0000-0000-7000-8000-000000000023";
|
|
195
|
+
const oldRolloutUpdatedPath = path.join(
|
|
196
|
+
sessionsDir,
|
|
197
|
+
`rollout-2026-04-15T01-00-00-${oldRolloutUpdatedSessionId}.jsonl`,
|
|
198
|
+
);
|
|
199
|
+
const newRolloutStalePath = path.join(
|
|
200
|
+
sessionsDir,
|
|
201
|
+
`rollout-2026-04-15T09-00-00-${newRolloutStaleSessionId}.jsonl`,
|
|
202
|
+
);
|
|
203
|
+
const untrackedFreshPath = path.join(sessionsDir, `rollout-2026-04-15T10-00-00-${untrackedFreshSessionId}.jsonl`);
|
|
204
|
+
|
|
205
|
+
await fs.writeFile(oldRolloutUpdatedPath, "{}\n", "utf8");
|
|
206
|
+
await fs.writeFile(newRolloutStalePath, "{}\n", "utf8");
|
|
207
|
+
await fs.writeFile(untrackedFreshPath, "{}\n", "utf8");
|
|
208
|
+
await fs.utimes(oldRolloutUpdatedPath, new Date("2026-04-15T11:00:00Z"), new Date("2026-04-15T11:00:00Z"));
|
|
209
|
+
await fs.utimes(newRolloutStalePath, new Date("2026-04-15T09:00:00Z"), new Date("2026-04-15T09:00:00Z"));
|
|
210
|
+
await fs.utimes(untrackedFreshPath, new Date("2026-04-15T12:00:00Z"), new Date("2026-04-15T12:00:00Z"));
|
|
211
|
+
|
|
212
|
+
const parsed = parseWorkspaceSource({
|
|
213
|
+
sourceType: "workspace-file",
|
|
214
|
+
sourceLabel: "/tmp/workspace.json",
|
|
215
|
+
title: "workspace",
|
|
216
|
+
workspaceManifest: {
|
|
217
|
+
version: "1",
|
|
218
|
+
workspaceId: "orp-main",
|
|
219
|
+
tabs: [
|
|
220
|
+
{
|
|
221
|
+
title: "new-rollout-stale",
|
|
222
|
+
path: "/Volumes/Code_2TB/code/new-rollout-stale",
|
|
223
|
+
resumeCommand: `codex resume ${newRolloutStaleSessionId}`,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
title: "old-rollout-updated",
|
|
227
|
+
path: "/Volumes/Code_2TB/code/old-rollout-updated",
|
|
228
|
+
resumeCommand: `codex resume ${oldRolloutUpdatedSessionId}`,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
notes: "",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const report = buildWorkspaceTabsReport(
|
|
236
|
+
{
|
|
237
|
+
sourceType: "workspace-file",
|
|
238
|
+
sourceLabel: "/tmp/workspace.json",
|
|
239
|
+
title: "workspace",
|
|
240
|
+
},
|
|
241
|
+
parsed,
|
|
242
|
+
{ codexHome },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
report.tabs.map((tab) => tab.title),
|
|
247
|
+
["old-rollout-updated", "new-rollout-stale"],
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("buildWorkspaceTabsReport bubbles a project when one attached Codex session is freshest", async () => {
|
|
252
|
+
const tempDir = await makeTempDir();
|
|
253
|
+
const codexHome = path.join(tempDir, "codex-home");
|
|
254
|
+
const sessionsDir = path.join(codexHome, "sessions", "2026", "04", "16");
|
|
255
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
const projectAOlderSessionId = "019d0000-0000-7000-8000-000000000011";
|
|
258
|
+
const projectANewerSessionId = "019d0000-0000-7000-8000-000000000012";
|
|
259
|
+
const projectBSessionId = "019d0000-0000-7000-8000-000000000013";
|
|
260
|
+
|
|
261
|
+
const projectAOlderPath = path.join(sessionsDir, `rollout-2026-04-16T01-00-00-${projectAOlderSessionId}.jsonl`);
|
|
262
|
+
const projectANewerPath = path.join(sessionsDir, `rollout-2026-04-16T03-00-00-${projectANewerSessionId}.jsonl`);
|
|
263
|
+
const projectBPath = path.join(sessionsDir, `rollout-2026-04-16T02-00-00-${projectBSessionId}.jsonl`);
|
|
264
|
+
|
|
265
|
+
await fs.writeFile(projectAOlderPath, "{}\n", "utf8");
|
|
266
|
+
await fs.writeFile(projectANewerPath, "{}\n", "utf8");
|
|
267
|
+
await fs.writeFile(projectBPath, "{}\n", "utf8");
|
|
268
|
+
await fs.utimes(projectAOlderPath, new Date("2026-04-16T01:00:00Z"), new Date("2026-04-16T01:00:00Z"));
|
|
269
|
+
await fs.utimes(projectANewerPath, new Date("2026-04-16T03:00:00Z"), new Date("2026-04-16T03:00:00Z"));
|
|
270
|
+
await fs.utimes(projectBPath, new Date("2026-04-16T02:00:00Z"), new Date("2026-04-16T02:00:00Z"));
|
|
271
|
+
|
|
272
|
+
const parsed = parseWorkspaceSource({
|
|
273
|
+
sourceType: "workspace-file",
|
|
274
|
+
sourceLabel: "/tmp/workspace.json",
|
|
275
|
+
title: "workspace",
|
|
276
|
+
workspaceManifest: {
|
|
277
|
+
version: "1",
|
|
278
|
+
workspaceId: "orp-main",
|
|
279
|
+
tabs: [
|
|
280
|
+
{
|
|
281
|
+
title: "project-b",
|
|
282
|
+
path: "/Volumes/Code_2TB/code/project-b",
|
|
283
|
+
resumeCommand: `codex resume ${projectBSessionId}`,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
title: "project-a-old",
|
|
287
|
+
path: "/Volumes/Code_2TB/code/project-a",
|
|
288
|
+
resumeCommand: `codex resume ${projectAOlderSessionId}`,
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
title: "project-a-new",
|
|
292
|
+
path: "/Volumes/Code_2TB/code/project-a",
|
|
293
|
+
resumeCommand: `codex resume ${projectANewerSessionId}`,
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
notes: "",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const report = buildWorkspaceTabsReport(
|
|
301
|
+
{
|
|
302
|
+
sourceType: "workspace-file",
|
|
303
|
+
sourceLabel: "/tmp/workspace.json",
|
|
304
|
+
title: "workspace",
|
|
305
|
+
},
|
|
306
|
+
parsed,
|
|
307
|
+
{ codexHome },
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
assert.deepEqual(
|
|
311
|
+
report.tabs.map((tab) => tab.title),
|
|
312
|
+
["project-a-new", "project-a-old", "project-b"],
|
|
313
|
+
);
|
|
314
|
+
assert.equal(report.projects[0]?.path, "/Volumes/Code_2TB/code/project-a");
|
|
315
|
+
assert.equal(report.projects[0]?.sessionCount, 2);
|
|
316
|
+
assert.deepEqual(
|
|
317
|
+
report.projects[0]?.sessions.map((session) => session.title),
|
|
318
|
+
["project-a-new", "project-a-old"],
|
|
319
|
+
);
|
|
320
|
+
assert.equal(report.projects[1]?.path, "/Volumes/Code_2TB/code/project-b");
|
|
321
|
+
});
|
|
322
|
+
|
|
186
323
|
test("runWorkspaceTabs prints JSON without launch commands", async () => {
|
|
187
324
|
const tempDir = await makeTempDir();
|
|
188
325
|
const manifestPath = path.join(tempDir, "workspace.json");
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|