@logicseek/hello-cc 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ const fixtureFailures = new WeakSet();
57
57
  const fixtureCommandFailures = new WeakMap();
58
58
  const fixtureHttpFailures = new WeakMap();
59
59
  const fixtureShimRegistrationFailures = new WeakMap();
60
+ const fixtureTmuxGcFailures = new WeakMap();
60
61
  const managedTmuxSessions = new Set();
61
62
 
62
63
  const env = {
@@ -102,6 +103,9 @@ export function fixtureFailureError(message, commandResult = null, diagnosticOpe
102
103
  if (diagnosticOperation === 'shim-registration') {
103
104
  fixtureShimRegistrationFailures.set(error, fixtureShimRegistrationSummary(commandResult));
104
105
  }
106
+ if (diagnosticOperation === 'tmux-gc') {
107
+ fixtureTmuxGcFailures.set(error, fixtureTmuxGcSummary(commandResult));
108
+ }
105
109
  return error;
106
110
  }
107
111
 
@@ -117,10 +121,32 @@ export function fixtureFailureDiagnostic(error) {
117
121
  ...(fixtureHttpFailures.has(error) ? { http: fixtureHttpFailures.get(error) } : {}),
118
122
  ...(fixtureShimRegistrationFailures.has(error)
119
123
  ? { shimRegistration: fixtureShimRegistrationFailures.get(error) } : {}),
124
+ ...(fixtureTmuxGcFailures.has(error) ? { tmuxGc: fixtureTmuxGcFailures.get(error) } : {}),
120
125
  ...(terminalMarker ? { terminalMarker } : {})
121
126
  };
122
127
  }
123
128
 
129
+ export function fixtureTmuxGcSummary(observation = {}) {
130
+ const reasons = ['stale_hcc_managed_session', 'stale_rebind_cleanup_failed_session',
131
+ 'tmux_stored_evidence_incomplete', 'tmux_session_changed', 'tmux_session_reused',
132
+ 'tmux_session_id_changed', 'tmux_root_unknown', 'tmux_root_changed', 'tmux_pane_changed',
133
+ 'tmux_pane_not_dead', 'tmux_dead_process_pid_unknown', 'tmux_dead_process_pid_changed', 'tmux_process_not_confirmed_dead',
134
+ 'tmux_process_changed', 'tmux_clients_unknown', 'tmux_has_clients',
135
+ 'tmux_binding_subject_incomplete', 'tmux_binding_session_authority_changed',
136
+ 'tmux_binding_root_authority_changed', 'tmux_binding_pane_authority_changed',
137
+ 'tmux_owner_process_live', 'tmux_owner_process_incompatible', 'tmux_target_missing',
138
+ 'not_hcc_managed_name', 'hcc_root_mismatch', 'duplicate_db_binding', 'runtime_managed',
139
+ 'not_old_enough', 'old_runtime_target_changed'];
140
+ return Object.freeze(Object.fromEntries(['stale', 'peerFilter', 'deadBinding', 'deadEvent'].map(role => {
141
+ const value = observation?.[role];
142
+ return [role, Object.freeze({
143
+ candidate: typeof value?.candidate === 'boolean' ? value.candidate : null,
144
+ reason: reasons.includes(value?.reason) ? value.reason : 'unknown',
145
+ mode: ['dead_process', 'explicit_exit_live'].includes(value?.mode) ? value.mode : null
146
+ })];
147
+ })));
148
+ }
149
+
124
150
  export function fixtureShimRegistrationSummary(observation = {}) {
125
151
  // Public annotations retain only fixture-owned booleans and finite enums.
126
152
  // CLI tables, provider output, identifiers, paths and environment are excluded.
@@ -983,6 +1009,17 @@ async function assertTmuxGcPolicy() {
983
1009
  await waitFor(() => run('tmux', ['display-message', '-p', '-t', deadEventPane, '#{pane_dead}']).trim() === '1', 'dead event pane');
984
1010
  await waitFor(() => run('tmux', ['display-message', '-p', '-t', clientUnknownEventPane, '#{pane_dead}']).trim() === '1', 'client-unknown dead event pane');
985
1011
  await waitFor(() => run('tmux', ['display-message', '-p', '-t', deadBindingPane, '#{pane_dead}']).trim() === '1', 'dead binding pane');
1012
+ // Recent tmux hides pane_pid after PTY closure. Keep the destructive guard
1013
+ // strict: test deletion only when this host still supplies the original PID,
1014
+ // and require an explicit safe skip otherwise. Never substitute a saved PID.
1015
+ const retainsDeadPanePid = (pane, evidence) => {
1016
+ const pid = Number(run('tmux', ['display-message', '-p', '-t', pane, '#{pane_pid}']).trim());
1017
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
1018
+ if (pid !== evidence.old_process_identity.pid) fail('dead tmux fixture process changed before GC');
1019
+ return true;
1020
+ };
1021
+ const deadBindingGcAvailable = retainsDeadPanePid(deadBindingPane, deadBindingEvidence);
1022
+ const deadEventGcAvailable = retainsDeadPanePid(deadEventPane, deadEventEvidence);
986
1023
 
987
1024
  const attachTmux = realTmuxBin ? `${sh(realTmuxBin)} -L ${sh(tmuxSocketName)}` : 'tmux';
988
1025
  const client = spawnSync('tmux', ['new-session', '-d', '-s', `${tmuxSession}-gc-client`, '-e', `HCC_ROOT=${root}`, 'sh', '-lc', `unset TMUX; exec ${attachTmux} attach-session -t ${sh(attachedSession)}`], {
@@ -1000,21 +1037,35 @@ async function assertTmuxGcPolicy() {
1000
1037
  HCC_REGRESSION_TMUX_FAIL_CLIENT_SESSIONS: [clientUnknownEventSession, clientUnknownBindingSession].join(' ')
1001
1038
  };
1002
1039
  const dryRun = hcc(['tmux', 'gc', '--older-than', '14'], { env: gcEnv });
1040
+ const dryRunJson = hccJson(['tmux', 'gc', '--older-than', '14'], { env: gcEnv });
1041
+ const gcObservation = Object.fromEntries(Object.entries({
1042
+ stale: stalePeer, peerFilter: peerFilterPeer, deadBinding: deadBindingPeer, deadEvent: deadEventPeer
1043
+ }).map(([role, peer]) => {
1044
+ const candidate = dryRunJson.candidates.find(row => row.peer === peer);
1045
+ const row = candidate || dryRunJson.skipped.find(row => row.peer === peer);
1046
+ return [role, { candidate: Boolean(candidate), reason: row?.reason, mode: row?.gc_validation_mode }];
1047
+ }));
1003
1048
  if (!gcOutputHasPeer(dryRun, stalePeer) || !dryRun.includes('stale_hcc_managed_session') ||
1004
1049
  !gcOutputHasPeer(dryRun, peerFilterPeer) ||
1005
- !gcOutputHasPeer(dryRun, deadBindingPeer) ||
1006
- !gcOutputHasPeer(dryRun, deadEventPeer) || !dryRun.includes('stale_rebind_cleanup_failed_session')) {
1007
- fail(`tmux gc dry-run did not list stale DB-proven managed session:\n${dryRun}`);
1050
+ gcOutputHasPeer(dryRun, deadBindingPeer) !== deadBindingGcAvailable ||
1051
+ gcOutputHasPeer(dryRun, deadEventPeer) !== deadEventGcAvailable ||
1052
+ (deadEventGcAvailable && !dryRun.includes('stale_rebind_cleanup_failed_session'))) {
1053
+ fail(`tmux gc dry-run did not list stale DB-proven managed session:\n${dryRun}\n${JSON.stringify(gcObservation)}`,
1054
+ gcObservation, 'tmux-gc');
1055
+ }
1056
+ for (const [peer, available] of [[deadBindingPeer, deadBindingGcAvailable], [deadEventPeer, deadEventGcAvailable]]) {
1057
+ if (!available && !dryRunJson.skipped.some(row => row.peer === peer && row.reason === 'tmux_dead_process_pid_unknown')) {
1058
+ fail('tmux gc must retain dead panes whose process binding is unavailable', gcObservation, 'tmux-gc');
1059
+ }
1008
1060
  }
1009
1061
  for (const protectedPeer of [reusedBindingPeer, clientUnknownBindingPeer, eventPeer, reusedEventPeer, reusedProcessEventPeer, clientUnknownEventPeer, rootEventPeer, legacyEventPeer]) {
1010
1062
  if (gcOutputHasPeer(dryRun, protectedPeer)) {
1011
1063
  fail(`tmux gc dry-run treated live/unknown event evidence as removable (${protectedPeer}):\n${dryRun}`);
1012
1064
  }
1013
1065
  }
1014
- const dryRunJson = hccJson(['tmux', 'gc', '--older-than', '14'], { env: gcEnv });
1015
1066
  const candidateModes = new Map(dryRunJson.candidates.map((candidate) => [candidate.peer, candidate.gc_validation_mode]));
1016
1067
  if (candidateModes.get(stalePeer) !== 'explicit_exit_live' ||
1017
- candidateModes.get(deadBindingPeer) !== 'dead_process') {
1068
+ candidateModes.get(deadBindingPeer) !== (deadBindingGcAvailable ? 'dead_process' : undefined)) {
1018
1069
  fail(`tmux gc did not record strict binding validation modes:\n${JSON.stringify(dryRunJson.candidates, null, 2)}`);
1019
1070
  }
1020
1071
  if (runMaybe('tmux', ['has-session', '-t', staleSession]).status !== 0) {
@@ -1037,7 +1088,9 @@ async function assertTmuxGcPolicy() {
1037
1088
  }
1038
1089
 
1039
1090
  const removed = hcc(['tmux', 'gc', '--older-than', '14', '--yes'], { env: gcEnv });
1040
- if (!gcOutputHasPeer(removed, stalePeer) || !gcOutputHasPeer(removed, deadBindingPeer) || !gcOutputHasPeer(removed, deadEventPeer) ||
1091
+ if (!gcOutputHasPeer(removed, stalePeer) ||
1092
+ gcOutputHasPeer(removed, deadBindingPeer) !== deadBindingGcAvailable ||
1093
+ gcOutputHasPeer(removed, deadEventPeer) !== deadEventGcAvailable ||
1041
1094
  gcOutputHasPeer(removed, reusedBindingPeer) || gcOutputHasPeer(removed, clientUnknownBindingPeer) ||
1042
1095
  gcOutputHasPeer(removed, eventPeer) || gcOutputHasPeer(removed, reusedEventPeer) ||
1043
1096
  gcOutputHasPeer(removed, reusedProcessEventPeer) || gcOutputHasPeer(removed, clientUnknownEventPeer) ||
@@ -1048,11 +1101,11 @@ async function assertTmuxGcPolicy() {
1048
1101
  if (runMaybe('tmux', ['has-session', '-t', staleSession]).status === 0) {
1049
1102
  fail('tmux gc --yes did not delete stale DB-proven managed session');
1050
1103
  }
1051
- if (runMaybe('tmux', ['has-session', '-t', deadBindingSession]).status === 0) {
1052
- fail('tmux gc --yes did not delete a truly dead exact binding session');
1104
+ if ((runMaybe('tmux', ['has-session', '-t', deadBindingSession]).status !== 0) !== deadBindingGcAvailable) {
1105
+ fail('tmux gc --yes must delete only a truly dead exact binding session with its original PID');
1053
1106
  }
1054
- if (runMaybe('tmux', ['has-session', '-t', deadEventSession]).status === 0) {
1055
- fail('tmux gc --yes did not delete a confirmed-dead rebind cleanup session listed by dry-run');
1107
+ if ((runMaybe('tmux', ['has-session', '-t', deadEventSession]).status !== 0) !== deadEventGcAvailable) {
1108
+ fail('tmux gc --yes must retain rebind cleanup sessions lacking a confirmed process binding');
1056
1109
  }
1057
1110
  for (const protectedSession of [reusedBindingSession, clientUnknownBindingSession, eventSession, reusedEventSession, reusedProcessEventSession, clientUnknownEventSession, rootEventSession, manualSession]) {
1058
1111
  if (runMaybe('tmux', ['has-session', '-t', protectedSession]).status !== 0) {
@@ -1087,7 +1140,8 @@ async function assertTmuxGcPolicy() {
1087
1140
  fail(`tmux gc changed attached-client binding:\n${JSON.stringify(attachedBinding, null, 2)}`);
1088
1141
  }
1089
1142
  const deadBinding = peerBindingRow(deadBindingPeer);
1090
- if (!deadBinding || deadBinding.transport !== 'detached' || deadBinding.runtime_target !== null) {
1143
+ if (!deadBinding || deadBinding.transport !== (deadBindingGcAvailable ? 'detached' : 'tmux') ||
1144
+ deadBinding.runtime_target !== (deadBindingGcAvailable ? null : deadBindingPane)) {
1091
1145
  fail(`tmux gc did not detach truly dead binding:\n${JSON.stringify(deadBinding, null, 2)}`);
1092
1146
  }
1093
1147
  for (const [peer, pane] of [
@@ -5,6 +5,21 @@ import { DatabaseSync } from 'node:sqlite';
5
5
 
6
6
  const FAILURE_CODE = 'NATIVE_ACCEPTANCE_COMMIT_GAP';
7
7
  const hash = text => createHash('sha256').update(text).digest('hex');
8
+
9
+ // Retain enough to distinguish a conflict from a lost response without
10
+ // writing response text, drafts, authorization headers or action tokens.
11
+ export function summarizeNativeRetryResponse(status, body) {
12
+ const bytes = Buffer.from(body);
13
+ const summary = { status, bytes: bytes.length, bodySha256: hash(bytes), format: 'non-json', code: null };
14
+ if (bytes.length <= 65536) {
15
+ try {
16
+ const value = JSON.parse(bytes.toString('utf8'));
17
+ summary.format = 'json';
18
+ if (typeof value?.error?.code === 'string' && /^[A-Z][A-Z0-9_]{0,79}$/.test(value.error.code)) summary.code = value.error.code;
19
+ } catch { /* The hash still identifies an HTML, empty or malformed response. */ }
20
+ } else summary.format = 'oversized';
21
+ return summary;
22
+ }
8
23
  function readMesh(dbPath, read) {
9
24
  const db = new DatabaseSync(dbPath, { readOnly: true });
10
25
  try { return read(db); } finally { db.close(); }
@@ -62,12 +77,17 @@ export async function runNativeRetryChecks({ browser, base, token, root, dbPath,
62
77
  const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
63
78
  await context.addInitScript(browserInstrumentation);
64
79
  const page = await context.newPage(); watch(page);
65
- const requests = [], responses = [];
80
+ const requests = [], responses = [], responseDetails = [], responseReads = [];
66
81
  page.on('request', request => {
67
82
  if (request.method() === 'POST' && new URL(request.url()).pathname === route) requests.push(request.postDataJSON());
68
83
  });
69
84
  page.on('response', response => {
70
- if (response.request().method() === 'POST' && new URL(response.url()).pathname === route) responses.push(response.status());
85
+ if (response.request().method() !== 'POST' || new URL(response.url()).pathname !== route) return;
86
+ responses.push(response.status());
87
+ const summary = { status: response.status(), format: 'unavailable', code: null };
88
+ responseDetails.push(summary);
89
+ responseReads.push(response.body().then(body => Object.assign(summary, summarizeNativeRetryResponse(response.status(), body)))
90
+ .catch(() => { summary.format = 'unavailable'; }));
71
91
  });
72
92
  let observerContext, workerStarted = false, failure;
73
93
  try {
@@ -147,17 +167,21 @@ export async function runNativeRetryChecks({ browser, base, token, root, dbPath,
147
167
  await shot(page, viewport.name + '-native-retry-reconciled');
148
168
  assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2), true);
149
169
  assert.deepEqual(responses, [409, 200]);
170
+ await Promise.all(responseReads);
171
+ assert.equal(responseDetails[0].code, FAILURE_CODE);
172
+ assert.equal(responseDetails[1].format, 'json');
150
173
  receipt.scenarios.push({ viewport, peer, submissionId, messageId: message.message_id,
151
174
  owner: originalState.owner, generation: originalState.generation,
152
- requestCount: requests.length, responseStatuses: responses, expectedFailure: FAILURE_CODE,
175
+ requestCount: requests.length, responseStatuses: responses, responseDetails, expectedFailure: FAILURE_CODE,
153
176
  originalBodySha256: hash(original), laterDraftPreserved: true, observerRetryBlocked: true,
154
177
  messages: counts.messages, durableIntents: counts.intents, pendingEvents: counts.pendingEvents,
155
178
  deliveries: active.deliveries.length, userOriginRetained: true, providerInputs: adapter.sent });
156
179
  check('native retry ' + viewport.name + ': original durable submission repairs once; edited draft and observer fence survive');
157
180
  } catch (error) {
158
181
  failure = error;
182
+ await Promise.all(responseReads);
159
183
  receipt.failure = { viewport: viewport.name, peer, message: error.message, requestCount: requests.length,
160
- responseStatuses: responses, injected: retryFault.injected.filter(value => value.peer === peer) };
184
+ responseStatuses: responses, responseDetails, injected: retryFault.injected.filter(value => value.peer === peer) };
161
185
  try { await shot(page, viewport.name + '-native-retry-failure'); } catch { /* Preserve the original assertion. */ }
162
186
  throw error;
163
187
  } finally {