@quantiya/codevibe-codex-plugin 2.0.76 → 2.0.78
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/hooks/common.sh +25 -0
- package/libexec/resolve-hook-session.js +113 -0
- package/node_modules/@quantiya/codevibe-core/dist/appsync/appsync-client.d.ts +2 -0
- package/node_modules/@quantiya/codevibe-core/dist/appsync/sync-source-cursor.d.ts +3 -0
- package/node_modules/@quantiya/codevibe-core/dist/index.js +3 -3
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/cli.js +57 -22
- package/node_modules/@quantiya/codevibe-core/dist/tool-activity/consolidator.d.ts +3 -0
- package/node_modules/@quantiya/codevibe-core/package.json +1 -1
- package/package.json +2 -2
package/hooks/common.sh
CHANGED
|
@@ -59,6 +59,7 @@ fi
|
|
|
59
59
|
|
|
60
60
|
CODEVIBE_TMPDIR="${TMPDIR:-/tmp}"
|
|
61
61
|
LOG_FILE="${LOG_FILE:-${CODEVIBE_TMPDIR}/codevibe-codex-hooks.log}"
|
|
62
|
+
CODEVIBE_HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
62
63
|
|
|
63
64
|
log() {
|
|
64
65
|
local level="$1"
|
|
@@ -525,9 +526,27 @@ ensure_codex_daemon() {
|
|
|
525
526
|
return $rc
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
bind_codex_hook_session() {
|
|
530
|
+
local binding session environment rc=0
|
|
531
|
+
binding=$(printf '%s' "$1" | node "$CODEVIBE_HOOKS_DIR/../libexec/resolve-hook-session.js") || rc=$?
|
|
532
|
+
case "$rc" in
|
|
533
|
+
0) export CODEVIBE_HOOK_ALLOW_RECOVERY=1 ;;
|
|
534
|
+
2) export CODEVIBE_HOOK_ALLOW_RECOVERY=0
|
|
535
|
+
log "WARN" "Hook metadata unavailable; retaining existing-port POST only, daemon recovery disabled" ;;
|
|
536
|
+
*) log "WARN" "Hook has no unique live wrapper binding; not posting or starting recovery"; return 1 ;;
|
|
537
|
+
esac
|
|
538
|
+
IFS=$'\t' read -r session environment <<< "$binding"
|
|
539
|
+
if [ "$session" != "$CODEVIBE_CODEX_TMUX_SESSION" ]; then
|
|
540
|
+
log "INFO" "Corrected stale hook wrapper binding: $CODEVIBE_CODEX_TMUX_SESSION -> $session"
|
|
541
|
+
fi
|
|
542
|
+
export CODEVIBE_CODEX_TMUX_SESSION="$session"
|
|
543
|
+
export ENVIRONMENT="$environment"
|
|
544
|
+
}
|
|
545
|
+
|
|
528
546
|
send_to_server() {
|
|
529
547
|
local endpoint="$1"
|
|
530
548
|
local json_data="$2"
|
|
549
|
+
bind_codex_hook_session "$json_data" || return 1
|
|
531
550
|
|
|
532
551
|
local server_url curl_exit http_code response
|
|
533
552
|
server_url=$(get_server_url)
|
|
@@ -570,6 +589,10 @@ send_to_server() {
|
|
|
570
589
|
log "ERROR" "Local curl invocation failed (exit $curl_exit) — not replacing the Codex daemon"
|
|
571
590
|
return 1
|
|
572
591
|
fi
|
|
592
|
+
if [ "${CODEVIBE_HOOK_ALLOW_RECOVERY:-1}" = "0" ]; then
|
|
593
|
+
log "WARN" "Hook transport unavailable and binding unverified; not replacing or launching a daemon"
|
|
594
|
+
return 1
|
|
595
|
+
fi
|
|
573
596
|
|
|
574
597
|
# (#638) TRANSPORT failure = curl non-zero OR HTTP 000 (dead daemon / missing
|
|
575
598
|
# port file). Relaunch the daemon ONCE and retry the POST once (one relaunch
|
|
@@ -627,4 +650,6 @@ export -f _reuse_live_codex_daemon
|
|
|
627
650
|
export -f _codex_daemon_pid_on_port
|
|
628
651
|
export -f _codex_candidate_is_excluded
|
|
629
652
|
export -f ensure_codex_daemon
|
|
653
|
+
export CODEVIBE_HOOKS_DIR
|
|
654
|
+
export -f bind_codex_hook_session
|
|
630
655
|
export -f send_to_server
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { parse } = require('shell-quote');
|
|
6
|
+
const nativeUuid = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;
|
|
7
|
+
const validNativeId = (id) => typeof id === 'string' && /^[A-Za-z0-9_-]{1,180}$/.test(id);
|
|
8
|
+
const environmentFor = (value) => value === 'development' || value === 'experiment' ? value : 'production';
|
|
9
|
+
|
|
10
|
+
// pane_start_command uses tmux args_escape/vis, not JSON. Decode bytes without
|
|
11
|
+
// evaluating shell text; literal UTF-8 runs remain UTF-8 and octal runs are bytes.
|
|
12
|
+
function decodeTmuxCommand(command) {
|
|
13
|
+
if (!command.startsWith('"')) return command;
|
|
14
|
+
if (!command.endsWith('"')) throw new Error('incomplete tmux quoting');
|
|
15
|
+
const body = command.slice(1, -1);
|
|
16
|
+
const chunks = [];
|
|
17
|
+
const escapes = { '\\': 92, '"': 34, '$': 36, '~': 126, a: 7, b: 8, e: 27,
|
|
18
|
+
f: 12, n: 10, r: 13, s: 32, t: 9, v: 11 };
|
|
19
|
+
let cursor = 0;
|
|
20
|
+
const literal = (text) => {
|
|
21
|
+
if (text.includes('\\')) throw new Error('unknown tmux escape');
|
|
22
|
+
chunks.push(Buffer.from(text));
|
|
23
|
+
};
|
|
24
|
+
for (const match of body.matchAll(/\\([0-7]{1,3}|[\\"$~abefnrstv])/g)) {
|
|
25
|
+
literal(body.slice(cursor, match.index));
|
|
26
|
+
const value = /^[0-7]/.test(match[1]) ? parseInt(match[1], 8) : escapes[match[1]];
|
|
27
|
+
chunks.push(Buffer.from([value]));
|
|
28
|
+
cursor = match.index + match[0].length;
|
|
29
|
+
}
|
|
30
|
+
literal(body.slice(cursor));
|
|
31
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function inheritedFallback(nativeId, session, environment) {
|
|
35
|
+
if (!validNativeId(nativeId) || !/^codevibe-codex-[1-9]\d*$/.test(session || '')) return null;
|
|
36
|
+
// Preserve the old existing-port POST on unavailable metadata, but never
|
|
37
|
+
// launch/replace a daemon without a verified binding.
|
|
38
|
+
return { session, environment: environmentFor(environment), allowRecovery: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resumed Codex shell snapshots can retain a previous wrapper's environment.
|
|
42
|
+
// Match the hook's native id to an explicit, live wrapper resume command instead
|
|
43
|
+
// of launching recovery for that stale name. Never evaluate pane command text.
|
|
44
|
+
function wrapperCommand(session, command) {
|
|
45
|
+
if (!/^codevibe-codex-[1-9]\d*$/.test(session)) return null;
|
|
46
|
+
let words;
|
|
47
|
+
try {
|
|
48
|
+
command = decodeTmuxCommand(command);
|
|
49
|
+
words = parse(command);
|
|
50
|
+
if (words.length === 1 && typeof words[0] === 'string') words = parse(words[0]);
|
|
51
|
+
} catch { return { undecodable: true }; }
|
|
52
|
+
const statements = [[]];
|
|
53
|
+
for (const word of words) {
|
|
54
|
+
if (typeof word === 'object' && word.op === ';') statements.push([]);
|
|
55
|
+
else statements[statements.length - 1].push(word);
|
|
56
|
+
}
|
|
57
|
+
const hasExport = (value) => statements.some((s) => s.length === 2 && s[0] === 'export' && s[1] === value);
|
|
58
|
+
if (!hasExport(`CODEVIBE_CODEX_TMUX_SESSION=${session}`)
|
|
59
|
+
|| !hasExport('CODEVIBE_CODEX_PRIMARY_COMPANION=1')) return null;
|
|
60
|
+
const environment = statements.find((s) => s.length === 2 && s[0] === 'export'
|
|
61
|
+
&& typeof s[1] === 'string' && s[1].startsWith('ENVIRONMENT='));
|
|
62
|
+
const codex = statements.filter((s) => s[0] === 'codex');
|
|
63
|
+
if (!environment || codex.length !== 1 || !codex[0].every((w) => typeof w === 'string')) return null;
|
|
64
|
+
return { session, environment: environmentFor(environment[1].slice('ENVIRONMENT='.length)), args: codex[0].slice(1) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveHookSession(nativeId, inheritedSession, inventory, inheritedEnvironment) {
|
|
68
|
+
if (!validNativeId(nativeId)) return null;
|
|
69
|
+
const entries = inventory.split('\n').filter(Boolean).flatMap((line) => {
|
|
70
|
+
const [session, dead, ...command] = line.split('\t');
|
|
71
|
+
if (dead !== '0') return [];
|
|
72
|
+
const wrapper = wrapperCommand(session, command.join('\t'));
|
|
73
|
+
return wrapper ? [{ session, wrapper }] : [];
|
|
74
|
+
});
|
|
75
|
+
const wrappers = entries.filter((e) => !e.wrapper.undecodable).map((e) => e.wrapper);
|
|
76
|
+
const resumed = wrappers.filter((w) => w.args[0] === 'resume' && w.args[1] === nativeId);
|
|
77
|
+
if (resumed.length > 1) return null;
|
|
78
|
+
const inherited = wrappers.filter((w) => w.session === inheritedSession);
|
|
79
|
+
if (entries.filter((e) => e.session === inheritedSession).length === 1
|
|
80
|
+
&& entries.some((e) => e.session === inheritedSession && e.wrapper.undecodable)) {
|
|
81
|
+
return inheritedFallback(nativeId, inheritedSession, inheritedEnvironment);
|
|
82
|
+
}
|
|
83
|
+
// A live fresh/picker wrapper is not proven stale. Preserve it rather than
|
|
84
|
+
// redirecting its hooks merely because another wrapper resumes this history.
|
|
85
|
+
if (inherited.length === 1 && !(inherited[0].args[0] === 'resume'
|
|
86
|
+
&& nativeUuid.test(inherited[0].args[1] || '')
|
|
87
|
+
&& inherited[0].args[1].toLowerCase() !== nativeId.toLowerCase())) return inherited[0];
|
|
88
|
+
return resumed.length === 1 ? resumed[0] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
let input;
|
|
93
|
+
const emit = (binding) => {
|
|
94
|
+
if (!binding) { process.exitCode = 1; return; }
|
|
95
|
+
process.stdout.write(`${binding.session}\t${binding.environment}\n`);
|
|
96
|
+
if (binding.allowRecovery === false) process.exitCode = 2;
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
input = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
100
|
+
const socket = (process.env.TMUX || '').split(',')[0];
|
|
101
|
+
const args = socket ? ['-S', socket] : [];
|
|
102
|
+
if (socket && !socket.startsWith('/')) throw new Error('invalid tmux socket');
|
|
103
|
+
const inventory = execFileSync('tmux', [...args, 'list-panes', '-a', '-F',
|
|
104
|
+
'#{session_name}\t#{pane_dead}\t#{pane_start_command}'], {
|
|
105
|
+
encoding: 'utf8', timeout: 2000, maxBuffer: 1024 * 1024,
|
|
106
|
+
});
|
|
107
|
+
emit(resolveHookSession(input.session_id, process.env.CODEVIBE_CODEX_TMUX_SESSION, inventory, process.env.ENVIRONMENT));
|
|
108
|
+
} catch {
|
|
109
|
+
emit(inheritedFallback(input?.session_id, process.env.CODEVIBE_CODEX_TMUX_SESSION, process.env.ENVIRONMENT));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { resolveHookSession };
|
|
@@ -886,6 +886,8 @@ export declare class AppSyncClient {
|
|
|
886
886
|
selectCompanionSourceTransport(options: {
|
|
887
887
|
sessionId: string;
|
|
888
888
|
root?: string;
|
|
889
|
+
/** Only an existing immutable source may retain its original wire format. */
|
|
890
|
+
existingSessionGenerationId?: string;
|
|
889
891
|
}): Promise<{
|
|
890
892
|
protocol: 'LEGACY';
|
|
891
893
|
} | {
|
|
@@ -17,6 +17,9 @@ export interface DesktopSyncSourcePosition {
|
|
|
17
17
|
byteOffset: number;
|
|
18
18
|
anchor: DesktopSyncSourceAnchor | null;
|
|
19
19
|
discardUntilNewline: boolean;
|
|
20
|
+
/** Recovery hint only, never persisted: finish this linked PREPARING frame
|
|
21
|
+
* before the reader may discard a later backlog range. */
|
|
22
|
+
pendingFrameEndOffset?: number;
|
|
20
23
|
}
|
|
21
24
|
export interface DesktopSyncSourceGap {
|
|
22
25
|
startOffset: number;
|