Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
160300 stories
·
33 followers

The Job That Was Never There

1 Share

The job that was never there is the hardest one to explain. After the last post, my inbox filled up for four days. The hardest messages were not from people who had lost a job. They were from people who had gone outside, sat down, looked back, and realised they had never quite had one.

The Job That Was Never There the-hollow-building

A few weeks ago I published an interview with somebody who finished their company’s AI development plan and was let go eleven days later.

I expected messages about the training. Those came.

The other kind I did not expect. There were more of them. They were worse. And every one of them circled the same thing without naming it, so I am going to name it here.

Losing the building was not the painful part.

That came about six weeks later, from a distance, when they finally looked back and saw what had actually been filling each day.

The usual note, and this time it matters more. Everything here is blended across many messages, reshaped so that no person, company, industry or system is identifiable. Titles, numbers and details are changed. And nobody in this piece is a fraud. I want that on the record before you read a word of it, not after.

The Number

The Job That Was Never There the-uncollected-pigeonholes

I will start with the one I cannot put down.

Somebody wrote to me whose job, for six years, was a weekly number. It went out every Monday to a list of about forty people.

It took most of Thursday and all of Friday morning, because the sources disagreed and reconciling them required knowing exactly where every body was buried.

They knew.

They were good at it. By every measure that existed inside that company, they were excellent at it.

Two months after they were let go, they asked a former colleague to check something. They could not fully explain to me why.

Nobody had asked for the number.

Not once. Eight Mondays. Forty people on that list, and not one had noticed the email had stopped. Or they had noticed, and it had not been worth a message.

They said they sat with the laptop open for a long time.

They said the strange thing was that they were not angry. They were embarrassed, which they knew made no sense. And underneath the embarrassment was something worse.

It was relief.

They did not want to talk about the relief. I understand why. Relief means part of you already knew, and that part has been sitting quietly at the back of every Monday for six years, waiting to be proved right.

Eleven Leads and One Thing Being Led

The Job That Was Never There the-ring-of-rooms

Another message opened with a sentence I have thought about every day since.

I was the Regional Lead.

They wrote it the way you write something you are proud of. They should have been. It took eleven years.

Two paragraphs later, almost in passing, they listed the other people who sat in the same weekly meeting.

The Global Lead. The Product Lead. The Platform Lead. The Security Lead. The Delivery Lead. The Data Lead. The Governance Lead. The Transformation Lead. The Value Realisation Lead. A Regional Lead for a different region. A Chief of Staff belonging to one of the above.

Eleven job titles. One thing being led.

They wrote: “I counted them on the train home. Then I counted them again, because I assumed I had made a mistake.”

Now here is the part that should worry you, and it is not the part you are expecting.

Every one of those eleven was real. Every one worked hard. Every one of them could have explained to you, in detail, with evidence, exactly why their role existed and precisely how it differed from the other ten.

And they would all have been right.

Because each of those functions was invented on a Tuesday, by somebody sensible, solving a genuine problem that genuinely existed at the time.

Nobody has ever been given the job of removing one.

So they accumulate. A ring of invented functions forms around one real one, the way a pearl forms, slowly and without anybody’s permission, and after fifteen years nobody in the room can tell you which one was the grain of sand.

Which Brings Me to the Uncomfortable Bit

I have gone back and forth on this and landed somewhere I did not expect.

The people were innocent. The ecosystem was not.

You did not invent your title. You applied for it. Somebody described it to you across a table, and you believed them, because why on earth would you not.

You did the work you were handed. You were measured against objectives you did not write, by a person who did not write them either, using a framework nobody in the building could name the origin of.

You are not guilty of anything.

But you were standing inside something that had stopped being honest with itself long before you arrived, and it needed you to keep standing there in order to keep going.

That is not guilt. It is not innocence either.

There is no word for it, and I think the absence of that word is a large part of why none of these people could say any of this out loud until a stranger wrote a blog post.

The Tuesday You Already Knew

Here is something almost every one of them mentioned, and not one of them mentioned first.

They already knew.

Not fully. Not in a way you could act on. But there was a Tuesday, three or four or nine years ago, in the middle of something ordinary, when a thought arrived fully formed and completely unwelcome.

What am I actually doing.

And you pushed it back down. Instantly. Expertly. The way you push down a thought about your own death, because you had a mortgage and a review coming up and two people at home who needed you to be fine.

You did not lie to yourself.

You simply declined to finish a sentence.

You did not lie to yourself. You just chose, every day for years, not to finish one sentence.

I think almost everybody reading this has had that Tuesday. I suspect some of you had it recently.

And I think you did exactly what they did, which is the only sane thing available at the time, which is nothing at all.

Twenty Two Meetings and Four Sentences

The Job That Was Never There twenty-two-chairs

Somebody who had been a Senior Manager for Operational Excellence, a real title held by real people in a great many companies, which I have never once heard defined twice the same way.

In their final month, out of an instinct they could not name, they exported their own calendar.

Twenty two recurring meetings. Governance forums. Alignment calls. Steering committees. Working groups.

They went through them one at a time and asked the most dangerous question in corporate life.

In how many of these have I ever actually said anything?

Four.

In the other eighteen they were, in their own word, coverage. Their name on the invite meant the function was represented. The representation justified the function. The function justified the headcount. The headcount justified somebody more senior.

Nobody designed that. I need you to hear that clearly.

It accreted. Like coral. Beautiful, structural, and entirely made of things that used to be alive.

The Deck That Became a Department

The Job That Was Never There the-corridor-that-ends

Years ago, somebody noticed a real problem. A good observation, made by a person paying attention.

They wrote it up with evidence. It was well received, because it was correct.

They were asked to propose a solution. So they wrote a strategy document. Also well received.

Then came headcount. Two people. Then five. Then a Head of. Then a function on the org chart with a mission statement, quarterly objectives, and a shared inbox.

Somewhere in year three, the original problem was quietly solved by a platform migration that had nothing to do with any of them.

The function ran for another four years.

“I built a department out of being right once, and then spent four years protecting a thing whose reason had already gone. At the time it felt like leadership.”

It does not sound like failure to me. It sounds like almost every organisation I have ever walked into.

Eleven Months of Green

The Job That Was Never There the-green-facade

So many people wrote about status reporting that I could have stacked the messages.

A programme reports green. Then green. Then green.

Eleven consecutive months of green, on a dashboard seen by important people.

In month twelve it is cancelled.

Everybody knew by month three.

What none of them could do was be the first to write amber. Amber is not a colour in those meetings. It is an accusation. It points at a specific person’s judgment, and the room hears it that way however carefully you word it.

One of them told me about a colleague who did go amber. Early, honestly, with a plan attached.

That colleague was not punished. That would have been easier to describe.

They were offered support. There was a warm conversation about whether they were overloaded. Somebody senior was added to help them. Their scope reduced quietly over the following two quarters.

Everybody watched. Everybody learned. Nobody ever discussed it.

Green is not a measurement. Green is a social act.

The Boarding Pass

Somebody sent me a photograph of a boarding pass. It took me a moment to understand why, and then I found it almost unbearable.

They had been flown to another country for a two day alignment workshop. Sixteen people. Four flown in. A facilitator. Breakout groups. Sticky notes, first physical, then carefully transcribed onto a digital board so the outputs would be preserved.

They looked that board up recently.

Still there. Read only. Last opened four days after the workshop.

By them.

They were careful to tell me it was not a waste, and I believe them. They met people they went on to work well with for years. Two decisions came out of it that stuck.

Then they wrote this, and it is the line I keep returning to.

“If somebody had told me to stay home and just send an email, I would have argued. I would have argued hard. That is the part I have to sit with.”

The Award

Somebody won an internal award. A real one. Ceremony, photograph, their manager visibly proud in a way they told me they still think about, years later.

It was for delivering a migration ahead of schedule.

They know now, with the terrible clarity that only arrives from outside the building, that they migrated a system almost nobody used onto a platform almost nobody would use. On time. Under budget. Beautifully.

They still have it. They cannot throw it away and they cannot look at it.

So it lives in a box, which seems to me like exactly the right place for most of what this piece is about.

The One That Actually Hurts

Four separate people wrote to me about this, and every single one of them apologised for bringing it up.

You onboarded somebody.

A bright young person, three or four years ago. Genuinely talented. Delighted to be there. Nervous on the first morning in the way that is lovely to see.

And you taught them.

You explained which forum things get raised in. You showed them how to write a status update that is honest and survivable at the same time. You told them, kindly, patiently, with total sincerity, who has to be told before the meeting rather than during it.

You were a good mentor.

That is the terrible part. You were an excellent mentor. Several people have probably told you so.

You handed a twenty six year old the complete operating manual for a machine you had not yet noticed was hollow.

One of them wrote: “They are still there. They are doing my old job now. They are very good at it.”

I could not get that sentence out of my head for a week, and I am not sure I have managed it yet.

What Is Actually Happening Here

The cheap version of this argument is that these people were passengers and the market simply corrected.

That version is lazy, and it is cruel to people who worked extremely hard for a very long time.

Here is what I think is really going on.

Past a certain size, an organisation loses the ability to see value directly. It genuinely cannot tell, from the centre, whether the work at the edges produces anything.

So it measures what it can see. Attendance. Reports. Status. Delivery against a plan it wrote itself.

Activity.

And people, being reasonable, supply activity. Not cynically. Not as a scheme. They supply it because it is what gets noticed, and being noticed is how you keep a job, and keeping a job is how you feed the people in your house.

Every individual behaved rationally. The result was a building full of intelligent, sincere, hard working people producing artefacts for one another.

Manufactured work is not fraud. It is what grows in the gap between what an organisation values and what it is able to measure.

That gap has been widening for twenty years. And now something has arrived that produces artefacts instantly, for free, in unlimited quantity.

The gap is no longer possible to look away from.

The Cruel Part Nobody Warns You About

Several of them worked this out alone, in different countries, at different times. Each thought it was a private discovery.

The skills that kept you employed there do not exist anywhere else.

Knowing which forum to raise something in. Knowing who has to be told before the meeting rather than during it. Writing a status update that is honest and survivable at the same time. Knowing whose sign off is real and whose is decorative.

Those are genuine skills. They are hard won. Several of the people who wrote to me were world class at them.

None of it transfers. Not one item.

It is all local knowledge about one building, and the building is gone.

Which produces the specific cruelty in all of this. The people who were best at surviving that system are the ones it left with the least, and they cannot even name the problem in an interview, because the honest sentence is:

“I was extremely good at something I can no longer describe.”

The Sentence You Lost

And here is a thing that never comes up in a redundancy conversation, because there is no box for it on the form.

You did not only lose a job.

You lost a sentence.

Regional Lead was not a title. It was the sentence your parents used when their friends asked what you did. It was the answer you gave at your cousin’s wedding when somebody asked. It was on a lanyard that your child once wore around the house for an entire afternoon because they thought it was hilarious.

That sentence is gone. There is nothing to put in its place yet.

And you still have to attend things. People still ask.

Nobody prepares you for how much of your identity was living inside a sentence you did not even write.

The Message That Was Not From Somebody Laid Off

The Job That Was Never There one-lit-window

One of them still had their job.

It arrived at eleven at night. It was four lines long.

They had read the piece at their desk, twice, and then sat in their car in the car park for a while before driving home. They wanted me to know that. They asked me not to reply, which I have honoured until now.

The last line was this.

“I am not laid off. I am worse. I know.”

If that is you, reading this at your desk right now with the door open behind you, I do not have anything clever. I have only this. Knowing is the hard part and you have already done it. Everything after knowing is just decisions, and decisions are survivable.

The Question I Have Started Asking

I am not going to hand you a framework. Every person in this piece is cleverer than a framework.

But there is one question I now ask clients, and it is rude, and I ask it anyway.

If this person went on leave tomorrow and nobody backfilled them, what breaks, and how long before anybody outside this team notices?

The point is not to be indispensable. Indispensable people are a risk, and usually an unhappy one.

The point is that somebody should know the answer. In most places I visit, nobody has ever asked.

And the version for you, on a Monday, is smaller and harder.

Open last week’s calendar. Find the one thing you did that would still have been worth doing if the company had not existed.

If you find it, protect it. That is the part that travels with you.

If you cannot find one, you have not learned something shameful about yourself. You have learned something about the shape of the room you are standing in.

And it is very much better to learn that on a Monday morning with a coffee than eight weeks after somebody takes your laptop.

The Thing They All Did Without Noticing

The Job That Was Never There the-second-cup

I want to end somewhere kinder, and I can, because there is something in these messages that surprised me and not one of them saw themselves doing it.

Every single person, at some point in their email, stopped talking about the work.

And started talking about a person.

The colleague who quietly covered for them the fortnight their parent was ill. The one who always made two teas without being asked. The one who explained the same thing four times and never once let them feel stupid about it. The Friday messages.

Not one of those was invented. Not one was manufactured. Nobody was ever inserted into a meeting to justify headcount and accidentally produced eleven years of friendship.

The building was hollow. The people standing in it were not.

If you take one thing from all of this, take that one.

The work may or may not have been real. That question is worth asking and you should ask it.

But the person who noticed you were having a bad day and said nothing about it and simply put a coffee on your desk was completely real. They are still real. Nothing that happened to your company changed that.

And you can message them tonight.

This is the question underneath all thirty essays in my book AI: Nobody’s in There. But we’re still in here. All thirty are free to read at pinaldave.com, and the book is available in paperback, Kindle and audiobook on Amazon.

One last message, and then I will stop.

It came at the end of a very long email describing a career I would have been proud of, containing real achievements that I could see clearly and the writer could not see at all.

The final line was this.

“I keep telling people I lost my job. I do not think that is what happened. I think I found out.”

This is not a story about people who were not needed, it is a story about a building that could not tell the difference and quietly asked them to prove it every single day.

Reference: Pinal Dave (https://blog.sqlauthority.com/), Layoffs and Meaningful Work, X

First appeared on The Job That Was Never There

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Journal 200 - State of Open Source

1 Share
It feels like open source has fallen off a cliff this year. It’s been on a downhill slide the past few years with all the corporate shenanigans. You have the hyperscalers implementing cloud services around open source projects while contributing little to nothing in return, and you have investor-backed “open source” companies that use a permissive license initially to build a community and then performing a licensing change rug pull to make the project proprietary. If all that wasn’t bad enough, now we’ve heaped on a steaming pile of AI slop in the form of drive-by contributions and a non-stop procession of CVEs. I just don’t see how this will be sustainable under the original principles of open source and free software.
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Understanding ChatGPT Work

1 Share

OpenAI announced ChatGPT Work on July 9th, and have been furiously iterating on it ever since. It is an extraordinarily confusing and very powerful product. Here's what I've figured out about it so far.

ChatGPT Work is actually two products

The more interesting version of ChatGPT Work is the one that runs in the cloud. This can be accessed via chatgpt.com or through the ChatGPT mobile apps. Let's call it Work Cloud.

If you install the ChatGPT desktop app - the app that used to be called Codex - you gain access to a thing called ChatGPT Work that can access files and run programs directly on your computer. Let's call that one Work Local. This one feels more like regular Codex re-skinned to be less intimidating to non-software-developers.

For the rest of this article I'm going to talk exclusively about Work Cloud.

Work is for paid subscribers only

Right now, ChatGPT Work (in both flavors) is available only to $20/month and up subscribers. Free users and $8/month Go users do not have access.

Work has features that aren't available in Chat

The interface for accessing Work is a tab selector, which presents it as an alternative to Chat:

ChatGPT app header with a Chat and a Work tab

The obvious question is when should I use Chat, and when should I use Work?

OpenAI's official answer to that question is:

Use Chat when you want an answer, explanation, brainstorm, or short draft. Use ChatGPT Work when you want ChatGPT to complete a task with a clear outcome, such as a brief, deck, analysis, recurring update, workflow, or file you can review and use.

I find that almost entirely useless, because I've been using regular ChatGPT Chat for all of those task categories for years!

The better question then is what features does Work have that are missing from Chat?

After extensive experimentation I think I've mostly figured that out:

Model selection

In Work, you get the option to pick GPT-5.6 Sol, Luna, or Terra, each with Light, Medium, High, Extra High, Max, or Ultra reasoning levels. You can also pick GPT-5.5 at Light, Medium, High, or Extra High.

These look to be the same models that are available through the OpenAI API.

Chat offers a different selection: 5.6 Instant, Medium, High, Extra High, and Pro (actually Extra High and Pro are only available for $100/month+ subscribers - $20/month subscribers cap out at High). It doesn't explain if those are Luna or Terra or Sol (I'm assuming Sol?). 5.6 Pro appears to be exclusive to Chat, with no equivalent in Work.

My current understanding from using Codex is that Ultra is a special mode that more eagerly delegates to sub-agents.

I believe ChatGPT Work sessions are billed against your Codex allowance, while ChatGPT Chat Sessions get their own, separate allowance. This may help explain the model availability differences.

Code execution with Internet access!

As a long-time fan of the Code Interpreter pattern - pioneered by OpenAI in 2023 - this is by far the most exciting feature of ChatGPT Work (Cloud) for me.

The code execution environment can now talk to the rest of the internet!

ChatGPT Chat can't do this - if you ask it to install additional software packages or interact with websites or APIs that access will be blocked by the container proxy.

(Weirdly, back in January it grew the ability to install packages, but that doesn't seem to work any more. I wish they had better changelogs!)

Claude's equivalent container has allowed restricted internet access since it launched last September. Claude can install packages from PYPI and NPM and clone repositories from GitHub. But that is about it: the allowlist of domains is very short.

ChatGPT Work allows a whole lot more than that. It can be configured with a specific list of allowed domains, but the default appears to be open to all.

This makes Work an incredibly useful tool. You can have it clone GitHub repositories, install their dependencies, then use them to interact with the rest of the web!

A full, headless Chrome browser

Another killer feature of ChatGPT Work is the browser tool. ChatGPT Work can launch a full Chrome instance, load websites, fill out forms, and take screenshots.

Screenshot of a ChatGPT conversation. A user message in a black rounded bubble reads: Visit https://london-pelicans-in-her-piety.simonw.chatgpt.site/ and take a screenshot with you browser. Below it a collapsed status line reads "Worked for 1m 18s >", followed by the reply "Here's the screenshot of the live site:" and an embedded screenshot of a website.

If a site requires sign in the browser can prompt you to take over and enter both passwords and 2FA codes, without round-tripping those credentials through the model itself.

It can even run JavaScript against the DOM of loaded pages. I prompted:

Load simonwillison.net in your browser and extract the headings using JavaScript

ChatGPT Work fired up a browser instance and ran the code:

await tab.playwright.evaluate(() => {
  return Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"), heading => ({
    level: heading.tagName.toLowerCase(),
    text: heading.innerText.trim().replace(/\s+/g, " "),
    id: heading.id || null
  }));
});

This feels a lot like my shot-scraper javascript tool, only now I can access it on my phone!

A persistent, shared filesystem

ChatGPT Chat gets a fresh filesystem for each chat session. These cannot be accessed from any other session.

In ChatGPT Work each session gets its own scratch folder - named something like /workspace/scratch/e00a0a017944 - but each of those are persisted across sessions, so you can access files from previous chats. I have 171 folders in /workspace/scratch right now!

As far as I can tell that /workspace volume is mounted to all Work sessions that are currently running - file edits from one can be instantly seen by the others. They don't seem to share the same process space though, and localhost servers running in one can't be accessed from another.

ChatGPT Sites

ChatGPT Work has the ability to build and deploy entire websites, using Cloudflare Workers. These can have HTML and JavaScript and can run server-side features too, including stateful features on top of Cloudflare D1 and R2.

Here's a simple site I built with this feature:

london-pelicans-in-her-piety.simonw.chatgpt.site

Screenshot of a website homepage on a cream background. Top navigation bar: a circular logo reading "P/P" on the left, the links "THE CENSUS", "COLLECTIONS" and "METHOD" in the center, and "JSON ↓" on the right. The left half is a hero section with small red capitals reading "AN ICONOGRAPHIC CENSUS · GREATER LONDON" above a large serif heading "Pelicans in her piety", with "piety" set in red italics. Below it: "Across London, an impossible bird bleeds for her young—in limewood, marble, mosaic, metal and glass. This is an evidence-backed census of where to find her." Two buttons follow: a solid black "EXPLORE ALL 28" and an outlined "DOWNLOAD THE DATA". The right half is a photograph of an ornate dark carved wooden reredos in a church, with gilded urns and a crest on top, Corinthian columns, a gilded pelican with outspread wings at its center above inscribed panels, an altar with a brass cross and red flowers, embroidered banners on either side, and a black-and-white checkerboard floor with red carpet. Vertical text along the photo's right edge reads "ST MARY ABCHURCH" and a caption at its bottom reads "Grinling Gibbons's reredos, St Mary Abchurch. Photograph: Diliff, CC BY-SA 3.0, via SPAB ↗". A statistics strip along the bottom shows "28 FIXED SITES", "4 COLLECTIONS", "3 OPEN LEADS" and "2 KNOWN LOSSES".

My prompt was:

Figure out all of the places in London with a pelican in her piety, then turn that into a JSON file and build a ChatGPT sites site about them

(A pelican in her piety is a fascinating piece of medieval Christian imagery - once you know about them you'll find them all over the place.)

These sites default to being private to the user that created them, but you can make them public and (on team plans) share them with other specific individuals.

Sub-agents with Sol, Luna, and Terra

There's not much to say about this one. ChatGPT Chat can't run sub-agents. ChatGPT Work can. This is very much a power-user feature: if you are running a complex project that can benefit from multiple parallel agents working together, Work can do that.

Scheduled prompt automations

Another feature that seems to have migrated from regular ChatGPT to ChatGPT Work at some point. You can prompt ChatGPT Work like this:

run a search to see if Waymo have announced a launch date for Half Moon Bay every day at 8am

This will schedule a prompt to run on that frequency. These prompts can decide that nothing interesting has happened, or they can decide to notify you of some new information.

Update: Actually this seems to work in ChatGPT Chat as well.

It's still worth noting here though, as it can be used in conjunction with other ChatGPT Work exclusive features. You can set a scheduled task to update a ChatGPT Site on an hourly basis, for example.

Is this safe?

An open question for me right now is how safe all of this stuff is.

My lethal trifecta model warns about the risks inherent in any agent system that combines access to private data with exposure to untrusted content and a way to communicate stolen information back to an attacker.

ChatGPT Work combines all three!

I'd love to hear more from OpenAI about how they protect ChatGPT Work sessions against prompt injection attacks. I expect their answer is the same auto-review mechanism as Codex.

OpenAI could make this a lot less confusing

Figuring this all out took way more work than it should have.

I think there are two key problems here:

  1. OpenAI explain Work in terms of what it's for, not what it actually does
  2. OpenAI still insist on hiding their system prompts and tools descriptions

If the ChatGPT Work documentation included the exact system prompt and tool descriptions used by the agent I wouldn't have needed to write this post.

Tags: ai, openai, generative-ai, chatgpt, llms, code-interpreter, lethal-trifecta, general-agents

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Why Mouse Jigglers Defeat Activity-Based Time Tracking

1 Share

Activity percentages on time-tracking dashboards are basically a compressed view of mouse and keyboard input. Hardware jigglers, software “movers,” and keep-awake scripts exist because they game that view: they keep the green bar green without proving anyone is working. Richer Windows endpoint data—apps, session state, files, network—can make fake presence look thin. Correlation still isn’t proof. If your company treats activity % as honesty, you’re measuring the wrong thing.


This got loud when remote and hybrid work turned “presence” into a management proxy. Billing fights, contractor timesheets, and always-on Slack status rewarded meters that look objective. A cheap USB gadget that nudges the cursor—or a script that fakes input—isn’t a clever attack on your stack. It’s a rational response to a bad metric. The dashboard isn’t broken. The metric was never presence.

Three questions managers keep collapsing into one

Most arguments about idle time and jigglers start the same way: leaders ask three different questions and expect one number to answer all of them.

  1. Was there input? Mouse moves, clicks, keypresses in a time window.
  2. Was the person actually working? Foreground apps, websites, documents, meeting context.
  3. Was there risk or a policy issue? USB copies, weird file paths, blocked sites, odd hours on sensitive data.


Activity % mostly answers question one—and not well. It can’t tell a human hand from a HID gadget that looks like a mouse, or from injected events. It also can’t tell “reading a long design doc on a second monitor” from “away from the desk.” Pure idle rules punish real work: light typing on video calls, long compiles, research that’s mostly scroll-and-think.


If the risk is payroll honesty, you need more than input volume. If the risk is data loss, mouse motion barely matters. Smash those goals into one green percentage, and jigglers win.

What SaaS “activity %” usually sees

A typical desktop agent for time tracking does some version of this:

HID / OS input events
        ↓
   Desktop agent
        ↓
  Time buckets (e.g. 1–10 min)
        ↓
   Activity percent
        ↓
     Dashboard


Windows already has basics for input idle detection. Microsoft’s GetLastInputInfo returns the time of the last input event for the session that called it—not magic proof that “the employee is working.” Agents may use last-input APIs, raw input hooks, or their own sampling of moves and keys. Same bottom line either way: the meter counts motion in a bucket, then paints a percentage.


That percentage does not prove:

  • real focus or meeting attention,
  • that someone’s at the desk,
  • that a human made the input,
  • that the work was billable or allowed by policy.

Jigglers win because they feed the same event class the meter counts. You don’t need a novel exploit. You need the sensor’s blind spot.

A short taxonomy (for detection—not a how-to)

ClassWhat it fakesWhat it usually fails to fake
USB HID “mouse” / moverSteady move or clickApp switching, file/network patterns, normal task hopping
Software mover / scriptPeriodic cursor wiggles or fake inputNatural timing, real typing, a believable app story
OS keep-awake / sleep blockersStops sleep / dimmingHigh activity % on its own—unless you also spoof input


This is a cheat sheet for ops and security, not a recipe book. The story is already public: banks and big employers have fired people over simulated keyboard activity and “mouse movers,” including Wells Fargo, as reported by the BBC. The takeaway isn’t “buy a detector.” It’s that if you treat input as proof of work, you create demand for tools that fake input.


Some monitoring vendors pitch dedicated “jiggler detection.” Treat that as marketing until you know which signals they use. Motion-only heuristics are brittle. Multi-signal context is where suspicion gets useful.

What richer endpoint telemetry can catch

Once you instrument a Windows PC beyond “how much did the mouse move,” you get a stack. Each layer does a different job. None of them is a courtroom.

LayerExamplesUseful against jigglers?
Input volume onlyMove/click/key countsNo—this is what jigglers target
Session stateLock/unlock, console vs remote, idle timeoutSomewhat—locked + “active” deserves a look
Process / app timelineForeground app, launch/exitYes—hours “active” on one junk process looks thin
Web / search (sensitive)URLs, queriesYes—heavy on privacy; policy first
File / USB / print / clipboardChannel activityStrong for fraud and DLP; different goal than time cheating
NetworkInterfaces, per-app trafficHelps separate real work from empty motion
Screens / live viewSpot checksHuman review; too sensitive for first-line payroll fights
Anomaly / rule hitsAlerts in a queueOps workflow—not a magic classifier


Hard truth: more signals raise suspicion. They don’t prove guilt. A quiet engineer on a call can look “idle.” A jiggler with a real IDE open can look “busy.” Treat multi-signal outliers as a review queue, not an auto-fire pipeline.


A simple investigation pattern:

  1. Flag — high activity with almost no app variety, or activity while the session is locked.
  2. Context — which apps and sites were up front? Any file/USB/web events? Was the agent offline earlier?
  3. Talk to a human — walk the timeline with a manager or the employee before you call it fraud.
  4. Decide — coaching, stop worshipping activity %, or escalate if the evidence holds.


Watch session boundaries too. Last-input APIs are session-specific. RDP and multi-session hosts (terminal servers / RDS) make “was this the interactive user?” a real engineering problem, not a dashboard footnote. On shared Windows hosts, per-session context beats one PC-wide activity bar. Sample the wrong session and you’ll mark someone idle forever—or credit them for activity that wasn’t theirs.

For the longer arc—from punch clocks to screenshots to “work intelligence”—see this Jon Stojan time tracking piece on HackerNoon. Vendors keep renaming the dashboard. The mistake stays the same when one thin sensor becomes a moral score.


Security teams that care about insider risk already think in layers. NIST SP 800-53 talks about continuous monitoring and audit as control families—not as one productivity percentage. Steal that mindset: pick controls that match the risk, then store as little as you can. Need attendance trends? Don’t keep keystroke content. Need DLP? Don’t pretend mouse percentages are your control set.

False positives, ethics, and policy

Obsessing over “catch jigglers” creates workplace theater. People learn to look busy. Trust drops. Then you get a second arms race: more invasive capture vs. smarter spoofing.


A healthier setup starts with a written policy:

  • What do you measure, and why?
  • Who can see raw screens or keystrokes, if anyone?
  • How long do you keep sensitive data vs. aggregates?
  • What happens on a flag—conversation, coaching, investigation—before HR?


Transparent monitoring (people know the scope) and stealth modes are policy choices, not moral absolutes. Time-tracking buyers usually want softer defaults. Security buyers often need channel visibility—files, USB, web—for insider risk. One product toggle won’t fix both cultures. That’s how you end up with dashboards that punish people who read and miss people who leak.


There’s a security angle that doesn’t get enough airtime: unknown USB HID devices on managed laptops aren’t just a “productivity cheat.” They’re an endpoint hygiene problem. If you already lock down flash drives, treat mystery HID gadgets with similar seriousness. That doesn’t mean hunting every accessibility tool or specialty mouse. It means inventory, allowlisting, and clear exceptions.


This isn’t legal advice. Notice, minimization, and retention rules vary by place. Put the employee-facing policy in writing before you turn agents on.

Practical checklist for security and ops

If the goal is payroll / billable honesty

  • Don’t use activity % alone in a dispute.
  • Cross-check app/website timelines, idle gaps, and calendar or ticket context when you have it—before you accuse anyone.
  • High activity + almost no app diversity → review queue, not auto-fire.
  • For knowledge work, prefer outcomes and deliverables. Use telemetry to explain weird patterns, not to grade people like machines.

If the goal is insider risk / DLP

  • Prioritize channel signals (files, USB, web, clipboard) and rule hits over mouse motion.
  • Use screen capture or live view after a signal—not as the default story of “how we manage people.”
  • Keep “was someone present?” separate from “did sensitive data leave?” in both tools and process.

Instrumentation hygiene

  • Log session lock, remote vs. console, and agent health. Offline gaps explain more than fake green bars.
  • Keep aggregates longer than raw keystrokes or wall-to-wall screen history when policy allows—less creep, still useful trends.
  • Write down what you measure in the employee-facing policy before deploy.
  • If hardware jigglers are a real concern, control unknown USB HID via endpoint policy—without turning every mouse into a witch hunt.

Conclusion

Mouse jigglers still fool time-tracking dashboards because those dashboards often measure input motion, then ask managers to treat the number as work. Endpoint telemetry across apps, session state, files, and network can make fake presence look thin—but only as correlation, and only if you respect false positives.


Measure the behavior that matches the business risk, not the easiest sensor.


What single metric does your company still treat as truth—and what would you replace it with? Drop it in the comments.


Read the whole story
alvinashcraft
11 hours ago
reply
Pennsylvania, USA
Share this story
Delete

AI's 'Creepy' Crawlers Criticized by Linux Foundation's IT Infrastructure Director

1 Share
The Linux Foundation's director of IT infrastructure says they now spend more CPU cycles "rendering commits for scrapers than we spend on all other kinds of legitimate access." At any one time, across 5 geo-distributed nodes, there are 14 CPU cores doing nothing but rendering git commits as html.... [W]hen a source is guaranteed to be LLM-free, like the entire history of kernel commits, it's worth its weight in gold as a source of training data... At the time of writing, linux.git is about 1.48 million commits. Oh, and we have about 922 forks of it on git.kernel.org — but don't worry, it's actually extremely efficient on the backend, since it's mostly the same objects in every fork. Unless, of course, you're a scraper, in which case you have, oh, several BILLION valid URLs you can scrape, only to get 922 duplicates of the same 1.48 million commits — which is exactly what the scrapers are doing. But wait, it's not just commits itself. You can also ask for patches, plain renders, diffs between arbitrary commits — cgit is happy to let you, which was perfect for the times when the Internet was for humans or crawlers who obeyed robots.txt, and is AWFUL right about now, because we can generate 1.2 METRIC BAJILLION valid URLs just for a single fork of linux.git. Initially, this was the solution — look through the logs, find out which IPs are obvious scraper bots, and fail2ban them. At first, this was easy, because the bots helpfully told you who they were via their user-agent. Then, they wised up and started pretending that they were random vanilla browsers. So, we started banning them by IP — after all, it's easy to figure out that an IP that is trying to grab every possible commit in a 8-year-old abandoned fork of linux is not really some lone Chrome on Windows user who is just furiously clicking every link that comes across their screen. The bots then started fanning out to entire subnets, but this was still meh, because obviously an IP coming from Google Compute is just pretending to be a Firefox user... And... that's when things turned really, really ugly. Suddenly, the crawlers were coming from millions of random residential or mobile IPs, all pretending to be random modern browsers. An IP like that would make 4-5 requests and then never show up in the logs again... They descended like swarms of locust, hit hard and fast until the system fell over and then moved on to the next target until you recovered. Then, they returned. Rinse. Repeat. They still do that — welcome to the wonderful world of "proxy SDK monetization." It's big business, and your TV is probably doing it... Today, git.kernel.org receives about 6M daily requests demanding to see random commits. Of these, 66% are still immediately batted away with the Anubis challenge, but 33% are now solving the math and getting through to the main site — because apparently what we have to offer is worth spending a ton of cycles to calculate the Anubis challenge... With a bunch of generous assumptions, legitimate requests are only about 2% of git.kernel.org traffic — everything else are scrapers... [W]e're turning off features to reduce the number of crawlable URLs and to gate off actions that are expensive for us to run. Expect to lose some functionality, at least when accessing our resources anonymously. Trust me, we hate it just as much as you, but at this point it's a necessity... [W]e promise to still offer all of our data for download to anyone who asks. You just may have to jump through more hoops to get it. Sorry.

Read more of this story at Slashdot.

Read the whole story
alvinashcraft
11 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Production Flutter Networking Without the Boilerplate: Reactive Repositories with BlocSignal

1 Share

The Networking Architecture Dilemma in Production Flutter

If you survey ten seasoned Flutter developers about how they structure networking in production, you will almost certainly see the same multi-tiered pipeline:

┌────────────────────────────────────────────────────────────────────────┐
│               Traditional Flutter Networking Pipeline                  │
├────────────────────────────────────────────────────────────────────────┤
│ [Dio / HTTP Client] ─▶ [API Service] ─▶ [Repository Layer] ─▶          │
│                      [Cubit / BLoC] ─▶ [UI Builders & Banners]         │
└────────────────────────────────────────────────────────────────────────┘

The underlying architectural principles are sound: separation of concerns, testability, and isolating network transport details from UI widgets.

However, in practice, this classical layered stack demands an enormous amount of repetitive boilerplate:

  1. Async State Union Ceremony: Defining four separate state classes (Initial, Loading, Success(data), Failure(error)) or union types for every single API endpoint.
  2. Race Conditions & In-Flight Cancellation: When users type queries or switch tabs rapidly, requests finish out of order. Preventing stale responses requires complex Dio CancelToken plumbing or heavy rxdart switchMap streams.
  3. Offline Caching & "Stale-While-Revalidate": Showing cached data on Frame 1 while fetching fresh updates in the background usually requires database synchronization and stream merging logic.
  4. The Repository vs. Controller Divide: Repositories hold data and caching logic, while BLoCs or Cubits hold reactive state. Because Dart only allows single inheritance, developers end up maintaining two separate class hierarchies connected by verbose dependency injection glue.

With bloc_signals, we can preserve complete separation of concerns while eliminating 70% of the friction.

Let us examine how to architect a modern, clean, production-ready networking layer using CubitSignalMixin, HydratedMixin, and .toAsyncBlocSignal().

⚡ 1. Symmetrical Async Projection with .toAsyncBlocSignal()

In many cases, a feature only needs to fetch data from an endpoint and present it in the UI with loading and error states.

Instead of writing a custom Cubit and four distinct state classes, any Dart Future<T> converts directly into a BlocSignalBase<AsyncState<T>> with a single method call:

class UserProfileService {
  UserProfileService(this._dio);
  final Dio _dio;

  Future<UserProfile> fetchUserProfile(String userId) async {
    final response = await _dio.get('/users/\$userId');
    return UserProfile.fromJson(response.data as Map<String, dynamic>);
  }
}

In your presentation layer or view model:

// Converts Future<UserProfile> into a synchronous BlocSignalBase<AsyncState<UserProfile>>
final userProfileBloc = profileService
    .fetchUserProfile('user_123')
    .toAsyncBlocSignal();

Declarative Exhaustive UI Binding

Because AsyncState<T> is a sealed class hierarchy (AsyncLoading, AsyncData, AsyncError), you get compile-time exhaustive pattern matching in your Flutter widgets:

BlocSignalBuilder<BlocSignalBase<AsyncState<UserProfile>>, AsyncState<UserProfile>>(
  bloc: userProfileBloc,
  builder: (context, state) => switch (state) {
    AsyncLoading() => const Center(
        child: CircularProgressIndicator(),
      ),
    AsyncData(:final value) => ProfileDetailsView(user: value),
    AsyncError(:final error) => ErrorCard(
        message: error.toString(),
      ),
  },
)

No custom state classes, no manual try/catch event plumbing, and no FutureBuilder rebuild bugs.

🧬 2. Reactive, Offline-Cached Repositories with CubitSignalMixin & HydratedMixin

In enterprise apps, repositories often need to extend an existing API client base class (such as BaseApiClient or AuthenticatedHttpService) while maintaining persistent local caches.

Because Dart only permits single inheritance, traditional repositories could not be state containers.

With CubitSignalMixin and HydratedMixin, your repository IS the reactive, persistent state container:

import 'package:bloc_signals/bloc_signals.dart';
import 'package:bloc_signals_hydrate/bloc_signals_hydrate.dart';
import 'package:dio/dio.dart';

/// A production domain repository extending BaseApiClient with 0ms reactivity & disk caching!
class ProductRepository extends BaseApiClient
    with
        CubitSignalMixin<AsyncState<List<Product>>>,
        HydratedMixin<AsyncState<List<Product>>> {
  ProductRepository(super.dio) {
    // 1. Initialize reactive signal container
    initCubitSignal(initialState: const AsyncLoading());

    // 2. Initialize Frame-1 persistent storage cache
    initHydrated(storageKey: 'cached_products_v1');
  }

  /// Refreshes data from the network using toFutureSignal to eliminate try-catch
  Future<void> refresh() async {
    emit(const AsyncLoading());

    // ⚡ toFutureSignal automatically projects success into AsyncData and exceptions into AsyncError!
    final fetchSignal = _fetchProducts().toFutureSignal();
    await fetchSignal.future;
    emit(fetchSignal.value);
  }

  Future<List<Product>> _fetchProducts() async {
    final response = await dio.get('/products');
    final rawList = response.data as List<dynamic>;
    return [
      for (final item in rawList) Product.fromJson(item as Map<String, dynamic>),
    ];
  }

  // 💾 Pattern matching for instant offline hydration:
  @override
  Object? toJson(AsyncState<List<Product>> state) => switch (state) {
        AsyncData(:final value) => [
            for (final product in value) product.toJson(),
          ],
        _ => null,
      };

  @override
  AsyncState<List<Product>>? fromJson(dynamic json) => switch (json) {
        {'products': List<dynamic> list} => AsyncData([
            for (final item in list) Product.fromJson(item as Map<String, dynamic>),
          ]),
        List<dynamic> list => AsyncData([
            for (final item in list) Product.fromJson(item as Map<String, dynamic>),
          ]),
        _ => null,
      };
}

What This Architecture Delivers:

  1. Instant Frame-1 Rendering: When the app opens, HydratedMixin restores the cached List<Product> synchronously before the first pixel renders. Zero loading flickers.
  2. Background Refresh: Calling repository.refresh() executes network I/O and updates the UI synchronously on emit(AsyncData(freshProducts)).
  3. Single Class Hierarchy: Extends BaseApiClient without needing an intermediate wrapper or proxy Cubit.

🛑 3. Eliminating Network Race Conditions with restartable()

One of the most insidious bugs in mobile networking is the out-of-order response.

If a user searches for "Fl", then "Flu", and finally "Flutter", the network request for "Fl" might take 800ms while "Flutter" takes 200ms. Without cancellation, the "Fl" response resolves last and overwrites the screen with stale data!

With BlocSignalMixin, solving this requires zero cancel tokens or Rx streams—just apply the built-in restartable() transformer:

sealed class SearchEvent {
  const SearchEvent();
}

final class SearchQueryChanged extends SearchEvent {
  const SearchQueryChanged(this.query);
  final String query;
}

class SearchRepository extends BaseApiClient
    with
        CubitSignalMixin<AsyncState<List<SearchResult>>>,
        BlocSignalMixin<SearchEvent, AsyncState<List<SearchResult>>> {
  SearchRepository(super.dio) {
    initCubitSignal(initialState: const AsyncData([]));

    // ⚡ Built-in concurrency control: automatically aborts prior in-flight queries!
    on<SearchQueryChanged>((event, emit) async {
      final query = event.query.trim();
      if (query.isEmpty) {
        emit(const AsyncData([]));
        return;
      }

      emit(const AsyncLoading());
      final searchSignal = _executeSearch(query).toFutureSignal();
      await searchSignal.future;
      emit(searchSignal.value);
    }, transformer: restartable());
  }

  Future<List<SearchResult>> _executeSearch(String query) async {
    final response = await dio.get('/search', queryParameters: {'q': query});
    final results = response.data as List<dynamic>;
    return [
      for (final item in results) SearchResult.fromJson(item as Map<String, dynamic>),
    ];
  }
}

🏛️ Summary Architecture Comparison

┌──────────────────────────────┬────────────────────────┬────────────────────────┐
│ Architectural Concern        │ Traditional Flutter    │ BlocSignal Ecosystem   │
├──────────────────────────────┼────────────────────────┼────────────────────────┤
│ Async State Representation   │ 4 custom classes/enums │ AsyncState<T> sealed   │
│ In-Flight Race Conditions    │ Dio CancelToken / Rx   │ restartable() builtin  │
│ Duplicate Tap Protection     │ Custom boolean flags   │ droppable() builtin    │
│ Frame-1 Offline Persistence  │ SQLite / SharedPreferences│ HydratedMixin frame-1│
│ Existing Base Class Interop  │ Proxy/Wrapper classes  │ CubitSignalMixin       │
└──────────────────────────────┴────────────────────────┴────────────────────────┘

By pairing pure Dart reactive signal primitives with composable mixins and higher-order concurrency transformers, your networking layer remains clean, testable, and robust—with a fraction of the traditional ceremony.

💬 Join the Discussion!

How do you currently handle cancellation, offline caching, and async state in your Flutter networking layer?

Share your architecture setups and thoughts in the comments below!

Read the whole story
alvinashcraft
11 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories